Add support for Exif and JPEG thumbnails
Also:
* This fixes a bug with jpeg compression when the color plane stride
doesn't match image width
* This fixes missing marker at the end of BLOB buffer.
Bug: 324383963
Test: atest virtual_camera_tests
Test: atest VirtualCameraTest
Test: camera cts
Test: Manually examining captured image with exiftool
Change-Id: I4db804b264a98b8873b5465d4d8d0b8477e9d9a5
diff --git a/services/camera/virtualcamera/util/JpegUtil.cc b/services/camera/virtualcamera/util/JpegUtil.cc
index 2b19c13..98f2448 100644
--- a/services/camera/virtualcamera/util/JpegUtil.cc
+++ b/services/camera/virtualcamera/util/JpegUtil.cc
@@ -19,7 +19,7 @@
#include <cstddef>
#include <cstdint>
-#include <memory>
+#include <optional>
#include <vector>
#include "android/hardware_buffer.h"
@@ -34,11 +34,9 @@
namespace virtualcamera {
namespace {
-constexpr int kJpegQuality = 80;
-
class LibJpegContext {
public:
- LibJpegContext(int width, int height, const size_t outBufferSize,
+ LibJpegContext(int width, int height, int quality, const size_t outBufferSize,
void* outBuffer)
: mWidth(width),
mHeight(height),
@@ -76,7 +74,7 @@
jpeg_set_defaults(&mCompressStruct);
// Set quality and colorspace.
- jpeg_set_quality(&mCompressStruct, kJpegQuality, 1);
+ jpeg_set_quality(&mCompressStruct, quality, 1);
jpeg_set_colorspace(&mCompressStruct, JCS_YCbCr);
// Configure RAW input mode - this let's libjpeg know we're providing raw,
@@ -94,11 +92,31 @@
mCompressStruct.comp_info[2].v_samp_factor = 1;
}
- bool compress(const android_ycbcr& ycbr) {
+ LibJpegContext& setApp1Data(const uint8_t* app1Data, const size_t size) {
+ mApp1Data = app1Data;
+ mApp1DataSize = size;
+ return *this;
+ }
+
+ std::optional<size_t> compress(const android_ycbcr& ycbr) {
+ // TODO(b/301023410) - Add support for compressing image sizes not aligned
+ // with DCT size.
+ if (mWidth % (2 * DCTSIZE) || (mHeight % (2 * DCTSIZE))) {
+ ALOGE(
+ "%s: Compressing YUV420 image with size %dx%d not aligned with 2 * "
+ "DCTSIZE (%d) is not currently supported.",
+ __func__, mWidth, mHeight, 2 * DCTSIZE);
+ return std::nullopt;
+ }
+
+ // Chroma planes have 1/2 resolution of the original image.
+ const int cHeight = mHeight / 2;
+ const int cWidth = mWidth / 2;
+
// Prepare arrays of pointers to scanlines of each plane.
std::vector<JSAMPROW> yLines(mHeight);
- std::vector<JSAMPROW> cbLines(mHeight / 2);
- std::vector<JSAMPROW> crLines(mHeight / 2);
+ std::vector<JSAMPROW> cbLines(cHeight);
+ std::vector<JSAMPROW> crLines(cHeight);
uint8_t* y = static_cast<uint8_t*>(ycbr.y);
uint8_t* cb = static_cast<uint8_t*>(ycbr.cb);
@@ -107,23 +125,27 @@
// Since UV samples might be interleaved (semiplanar) we need to copy
// them to separate planes, since libjpeg doesn't directly
// support processing semiplanar YUV.
- const int c_samples = (mWidth / 2) * (mHeight / 2);
- std::vector<uint8_t> cb_plane(c_samples);
- std::vector<uint8_t> cr_plane(c_samples);
+ const int cSamples = cWidth * cHeight;
+ std::vector<uint8_t> cb_plane(cSamples);
+ std::vector<uint8_t> cr_plane(cSamples);
// TODO(b/301023410) - Use libyuv or ARM SIMD for "unzipping" the data.
- for (int i = 0; i < c_samples; ++i) {
- cb_plane[i] = *cb;
- cr_plane[i] = *cr;
- cb += ycbr.chroma_step;
- cr += ycbr.chroma_step;
+ int out_idx = 0;
+ for (int i = 0; i < cHeight; ++i) {
+ for (int j = 0; j < cWidth; ++j) {
+ cb_plane[out_idx] = cb[j * ycbr.chroma_step];
+ cr_plane[out_idx] = cr[j * ycbr.chroma_step];
+ out_idx++;
+ }
+ cb += ycbr.cstride;
+ cr += ycbr.cstride;
}
// Collect pointers to individual scanline of each plane.
for (int i = 0; i < mHeight; ++i) {
yLines[i] = y + i * ycbr.ystride;
}
- for (int i = 0; i < (mHeight / 2); ++i) {
+ for (int i = 0; i < cHeight; ++i) {
cbLines[i] = cb_plane.data() + i * (mWidth / 2);
crLines[i] = cr_plane.data() + i * (mWidth / 2);
}
@@ -131,7 +153,7 @@
return compress(yLines, cbLines, crLines);
}
- bool compressBlackImage() {
+ std::optional<size_t> compressBlackImage() {
// We only really need to prepare one scanline for Y and one shared scanline
// for Cb & Cr.
std::vector<uint8_t> yLine(mWidth, 0);
@@ -165,11 +187,18 @@
// Takes vector of pointers to Y / Cb / Cr scanlines as an input. Length of
// each vector needs to correspond to height of corresponding plane.
//
- // Returns true if compression is successful, false otherwise.
- bool compress(std::vector<JSAMPROW>& yLines, std::vector<JSAMPROW>& cbLines,
- std::vector<JSAMPROW>& crLines) {
+ // Returns size of compressed image in bytes on success, empty optional otherwise.
+ std::optional<size_t> compress(std::vector<JSAMPROW>& yLines,
+ std::vector<JSAMPROW>& cbLines,
+ std::vector<JSAMPROW>& crLines) {
jpeg_start_compress(&mCompressStruct, TRUE);
+ if (mApp1Data != nullptr && mApp1DataSize > 0) {
+ ALOGV("%s: Writing exif, size %zu B", __func__, mApp1DataSize);
+ jpeg_write_marker(&mCompressStruct, JPEG_APP0 + 1,
+ static_cast<const JOCTET*>(mApp1Data), mApp1DataSize);
+ }
+
while (mCompressStruct.next_scanline < mCompressStruct.image_height) {
const uint32_t batchSize = DCTSIZE * 2;
const uint32_t nl = mCompressStruct.next_scanline;
@@ -181,11 +210,11 @@
ALOGE("%s: compressed %u lines, expected %u (total %u/%u)",
__FUNCTION__, done, batchSize, mCompressStruct.next_scanline,
mCompressStruct.image_height);
- return false;
+ return std::nullopt;
}
}
jpeg_finish_compress(&mCompressStruct);
- return mSuccess;
+ return mEncodedSize;
}
// === libjpeg callbacks below ===
@@ -217,6 +246,10 @@
jpeg_error_mgr mErrorMgr;
jpeg_destination_mgr mDestinationMgr;
+ // APP1 data.
+ const uint8_t* mApp1Data = nullptr;
+ size_t mApp1DataSize = 0;
+
// Dimensions of the input image.
int mWidth;
int mHeight;
@@ -235,15 +268,26 @@
} // namespace
-bool compressJpeg(int width, int height, const android_ycbcr& ycbcr,
- size_t outBufferSize, void* outBuffer) {
- return LibJpegContext(width, height, outBufferSize, outBuffer).compress(ycbcr);
+std::optional<size_t> compressJpeg(const int width, const int height,
+ const int quality, const android_ycbcr& ycbcr,
+ const std::vector<uint8_t>& app1ExifData,
+ size_t outBufferSize, void* outBuffer) {
+ LibJpegContext context(width, height, quality, outBufferSize, outBuffer);
+ if (!app1ExifData.empty()) {
+ context.setApp1Data(app1ExifData.data(), app1ExifData.size());
+ }
+ return context.compress(ycbcr);
}
-bool compressBlackJpeg(int width, int height, size_t outBufferSize,
- void* outBuffer) {
- return LibJpegContext(width, height, outBufferSize, outBuffer)
- .compressBlackImage();
+std::optional<size_t> compressBlackJpeg(const int width, const int height,
+ const int quality,
+ const std::vector<uint8_t>& app1ExifData,
+ size_t outBufferSize, void* outBuffer) {
+ LibJpegContext context(width, height, quality, outBufferSize, outBuffer);
+ if (!app1ExifData.empty()) {
+ context.setApp1Data(app1ExifData.data(), app1ExifData.size());
+ }
+ return context.compressBlackImage();
}
} // namespace virtualcamera
diff --git a/services/camera/virtualcamera/util/JpegUtil.h b/services/camera/virtualcamera/util/JpegUtil.h
index c44d0a8..e64fb4f 100644
--- a/services/camera/virtualcamera/util/JpegUtil.h
+++ b/services/camera/virtualcamera/util/JpegUtil.h
@@ -18,6 +18,7 @@
#define ANDROID_COMPANION_VIRTUALCAMERA_JPEGUTIL_H
#include <memory>
+#include <optional>
#include "android/hardware_buffer.h"
#include "system/graphics.h"
@@ -27,14 +28,34 @@
namespace virtualcamera {
// Jpeg-compress image into the output buffer.
-// Returns true if the compression was successful, false otherwise.
-bool compressJpeg(int width, int height, const android_ycbcr& ycbcr,
- size_t outBufferSize, void* outBuffer);
+// * width - width of the image
+// * heigh - height of the image
+// * quality - 0-100, higher number corresponds to higher quality.
+// * ycbr - android_ycbr structure describing layout of input YUV420 image.
+// * app1ExifData - vector containing data to be included in APP1
+// segment. Can be empty.
+// * outBufferSize - capacity of the output buffer.
+// * outBuffer - output buffer to write compressed data into.
+// Returns size of compressed data if the compression was successful,
+// empty optional otherwise.
+std::optional<size_t> compressJpeg(int width, int height, int quality,
+ const android_ycbcr& ycbcr,
+ const std::vector<uint8_t>& app1ExifData,
+ size_t outBufferSize, void* outBuffer);
// Jpeg-compress all-black image into the output buffer.
-// Returns true if the compression was successful, false otherwise.
-bool compressBlackJpeg(int width, int height, size_t outBufferSize,
- void* outBuffer);
+// * width - width of the image
+// * heigh - height of the image
+// * quality - 0-100, higher number corresponds to higher quality.
+// * app1ExifData - vector containing data to be included in APP1
+// segment. Can be empty.
+// * outBufferSize - capacity of the output buffer.
+// * outBuffer - output buffer to write compressed data into.
+// Returns size of compressed data if the compression was successful,
+// empty optional otherwise.
+std::optional<size_t> compressBlackJpeg(int width, int height, int quality,
+ const std::vector<uint8_t>& app1ExifData,
+ size_t outBufferSize, void* outBuffer);
} // namespace virtualcamera
} // namespace companion
diff --git a/services/camera/virtualcamera/util/MetadataBuilder.cc b/services/camera/virtualcamera/util/MetadataUtil.cc
similarity index 88%
rename from services/camera/virtualcamera/util/MetadataBuilder.cc
rename to services/camera/virtualcamera/util/MetadataUtil.cc
index 2bbd58c..d223f17 100644
--- a/services/camera/virtualcamera/util/MetadataBuilder.cc
+++ b/services/camera/virtualcamera/util/MetadataUtil.cc
@@ -15,9 +15,9 @@
*/
// #define LOG_NDEBUG 0
-#define LOG_TAG "MetadataBuilder"
+#define LOG_TAG "MetadataUtil"
-#include "MetadataBuilder.h"
+#include "MetadataUtil.h"
#include <algorithm>
#include <cstdint>
@@ -396,6 +396,22 @@
return *this;
}
+MetadataBuilder& MetadataBuilder::setJpegQuality(const uint8_t quality) {
+ mEntryMap[ANDROID_JPEG_QUALITY] = asVectorOf<uint8_t>(quality);
+ return *this;
+}
+
+MetadataBuilder& MetadataBuilder::setJpegThumbnailSize(const int width,
+ const int height) {
+ mEntryMap[ANDROID_JPEG_THUMBNAIL_SIZE] = std::vector<int32_t>({width, height});
+ return *this;
+}
+
+MetadataBuilder& MetadataBuilder::setJpegThumbnailQuality(const uint8_t quality) {
+ mEntryMap[ANDROID_JPEG_THUMBNAIL_QUALITY] = asVectorOf<uint8_t>(quality);
+ return *this;
+}
+
MetadataBuilder& MetadataBuilder::setMaxNumberOutputStreams(
const int32_t maxRawStreams, const int32_t maxProcessedStreams,
const int32_t maxStallStreams) {
@@ -597,6 +613,67 @@
return aidlMetadata;
}
+std::optional<int32_t> getJpegQuality(
+ const aidl::android::hardware::camera::device::CameraMetadata& cameraMetadata) {
+ auto metadata =
+ reinterpret_cast<const camera_metadata_t*>(cameraMetadata.metadata.data());
+
+ camera_metadata_ro_entry_t entry;
+ if (find_camera_metadata_ro_entry(metadata, ANDROID_JPEG_QUALITY, &entry) !=
+ OK) {
+ return std::nullopt;
+ }
+
+ return *entry.data.i32;
+}
+
+std::optional<Resolution> getJpegThumbnailSize(
+ const aidl::android::hardware::camera::device::CameraMetadata& cameraMetadata) {
+ auto metadata =
+ reinterpret_cast<const camera_metadata_t*>(cameraMetadata.metadata.data());
+
+ camera_metadata_ro_entry_t entry;
+ if (find_camera_metadata_ro_entry(metadata, ANDROID_JPEG_THUMBNAIL_SIZE,
+ &entry) != OK) {
+ return std::nullopt;
+ }
+
+ return Resolution(entry.data.i32[0], entry.data.i32[1]);
+}
+
+std::optional<int32_t> getJpegThumbnailQuality(
+ const aidl::android::hardware::camera::device::CameraMetadata& cameraMetadata) {
+ auto metadata =
+ reinterpret_cast<const camera_metadata_t*>(cameraMetadata.metadata.data());
+
+ camera_metadata_ro_entry_t entry;
+ if (find_camera_metadata_ro_entry(metadata, ANDROID_JPEG_THUMBNAIL_QUALITY,
+ &entry) != OK) {
+ return std::nullopt;
+ }
+
+ return *entry.data.i32;
+}
+
+std::vector<Resolution> getJpegAvailableThumbnailSizes(
+ const aidl::android::hardware::camera::device::CameraMetadata& cameraMetadata) {
+ auto metadata =
+ reinterpret_cast<const camera_metadata_t*>(cameraMetadata.metadata.data());
+
+ camera_metadata_ro_entry_t entry;
+ if (find_camera_metadata_ro_entry(
+ metadata, ANDROID_JPEG_AVAILABLE_THUMBNAIL_SIZES, &entry) != OK) {
+ return {};
+ }
+
+ std::vector<Resolution> thumbnailSizes;
+ thumbnailSizes.reserve(entry.count / 2);
+ for (int i = 0; i < entry.count; i += 2) {
+ thumbnailSizes.emplace_back(entry.data.i32[i], entry.data.i32[i + 1]);
+ }
+ return thumbnailSizes;
+}
+
} // namespace virtualcamera
} // namespace companion
} // namespace android
diff --git a/services/camera/virtualcamera/util/MetadataBuilder.h b/services/camera/virtualcamera/util/MetadataUtil.h
similarity index 90%
rename from services/camera/virtualcamera/util/MetadataBuilder.h
rename to services/camera/virtualcamera/util/MetadataUtil.h
index df99089..9ddfd81 100644
--- a/services/camera/virtualcamera/util/MetadataBuilder.h
+++ b/services/camera/virtualcamera/util/MetadataUtil.h
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-#ifndef ANDROID_COMPANION_VIRTUALCAMERA_METADATABUILDER_H
-#define ANDROID_COMPANION_VIRTUALCAMERA_METADATABUILDER_H
+#ifndef ANDROID_COMPANION_VIRTUALCAMERA_METADATAUTIL_H
+#define ANDROID_COMPANION_VIRTUALCAMERA_METADATAUTIL_H
#include <chrono>
#include <cstdint>
@@ -98,7 +98,8 @@
sensorReadoutTimestamp);
// See ANDROID_LENS_INFO_AVAILABLE_FOCAL_LENGTHS in CameraMetadataTag.aidl.
- MetadataBuilder& setAvailableFocalLengths(const std::vector<float>& focalLengths);
+ MetadataBuilder& setAvailableFocalLengths(
+ const std::vector<float>& focalLengths);
// See ANDROID_LENS_FOCAL_LENGTH in CameraMetadataTag.aidl.
MetadataBuilder& setFocalLength(float focalLength);
@@ -278,6 +279,15 @@
MetadataBuilder& setJpegAvailableThumbnailSizes(
const std::vector<Resolution>& thumbnailSizes);
+ // See JPEG_QUALITY in CaptureRequest.java.
+ MetadataBuilder& setJpegQuality(uint8_t quality);
+
+ // See JPEG_THUMBNAIL_SIZE in CaptureRequest.java.
+ MetadataBuilder& setJpegThumbnailSize(int width, int height);
+
+ // See JPEG_THUMBNAIL_QUALITY in CaptureRequest.java.
+ MetadataBuilder& setJpegThumbnailQuality(uint8_t quality);
+
// The maximum numbers of different types of output streams
// that can be configured and used simultaneously by a camera device.
//
@@ -353,8 +363,25 @@
bool mExtendWithAvailableCharacteristicsKeys = false;
};
+// Returns JPEG_QUALITY from metadata, or nullopt if the key is not present.
+std::optional<int32_t> getJpegQuality(
+ const aidl::android::hardware::camera::device::CameraMetadata& metadata);
+
+// Returns JPEG_THUMBNAIL_SIZE from metadata, or nullopt if the key is not present.
+std::optional<Resolution> getJpegThumbnailSize(
+ const aidl::android::hardware::camera::device::CameraMetadata& metadata);
+
+// Returns JPEG_THUMBNAIL_QUALITY from metadata, or nullopt if the key is not present.
+std::optional<int32_t> getJpegThumbnailQuality(
+ const aidl::android::hardware::camera::device::CameraMetadata& metadata);
+
+// Returns JPEG_AVAILABLE_THUMBNAIL_SIZES from metadata, or nullopt if the key
+// is not present.
+std::vector<Resolution> getJpegAvailableThumbnailSizes(
+ const aidl::android::hardware::camera::device::CameraMetadata& metadata);
+
} // namespace virtualcamera
} // namespace companion
} // namespace android
-#endif // ANDROID_COMPANION_VIRTUALCAMERA_METADATABUILDER_H
+#endif // ANDROID_COMPANION_VIRTUALCAMERA_METADATAUTIL_H
diff --git a/services/camera/virtualcamera/util/Util.cc b/services/camera/virtualcamera/util/Util.cc
index 2d0545d..ef986a6 100644
--- a/services/camera/virtualcamera/util/Util.cc
+++ b/services/camera/virtualcamera/util/Util.cc
@@ -79,6 +79,10 @@
return true;
}
+std::ostream& operator<<(std::ostream& os, const Resolution& resolution) {
+ return os << resolution.width << "x" << resolution.height;
+}
+
} // namespace virtualcamera
} // namespace companion
} // namespace android
diff --git a/services/camera/virtualcamera/util/Util.h b/services/camera/virtualcamera/util/Util.h
index 9f81bb1..15f7969 100644
--- a/services/camera/virtualcamera/util/Util.h
+++ b/services/camera/virtualcamera/util/Util.h
@@ -54,6 +54,7 @@
// Representation of resolution / size.
struct Resolution {
+ Resolution() = default;
Resolution(const int w, const int h) : width(w), height(h) {
}
@@ -69,10 +70,12 @@
return width == other.width && height == other.height;
}
- const int width;
- const int height;
+ int width = 0;
+ int height = 0;
};
+std::ostream& operator<<(std::ostream& os, const Resolution& resolution);
+
} // namespace virtualcamera
} // namespace companion
} // namespace android