blob: 14adeac998ee3b11fd12e44f9fec868fb31b1c7e [file] [log] [blame]
/*
* Copyright (C) 2023 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.
*/
#define FAILURE_DEBUG_PREFIX "jpeg"
#include <inttypes.h>
#include <setjmp.h>
#include <algorithm>
#include <vector>
extern "C" {
#include <jpeglib.h>
}
#include <libyuv/scale.h>
#include <system/camera_metadata.h>
#include "debug.h"
#include "exif.h"
#include "jpeg.h"
#include "yuv.h"
namespace android {
namespace hardware {
namespace camera {
namespace provider {
namespace implementation {
namespace jpeg {
namespace {
constexpr int kJpegMCUSize = 16; // we have to feed `jpeg_write_raw_data` in multiples of this
// compressYUVImplPixelsFast handles the case where the image width is a multiple
// of kJpegMCUSize. In this case no additional memcpy is required. See
// compressYUVImplPixelsSlow below for the cases where the image width is not
// a multiple of kJpegMCUSize.
bool compressYUVImplPixelsFast(const android_ycbcr& image, jpeg_compress_struct* cinfo) {
const uint8_t* y[kJpegMCUSize];
const uint8_t* cb[kJpegMCUSize / 2];
const uint8_t* cr[kJpegMCUSize / 2];
const uint8_t** planes[] = { y, cb, cr };
const int height = cinfo->image_height;
const int height1 = height - 1;
const int ystride = image.ystride;
const int cstride = image.cstride;
while (true) {
const int nscl = cinfo->next_scanline;
if (nscl >= height) {
break;
}
for (int i = 0; i < kJpegMCUSize; ++i) {
const int nscli = std::min(nscl + i, height1);
y[i] = static_cast<const uint8_t*>(image.y) + nscli * ystride;
if ((i & 1) == 0) {
const int offset = (nscli / 2) * cstride;
cb[i / 2] = static_cast<const uint8_t*>(image.cb) + offset;
cr[i / 2] = static_cast<const uint8_t*>(image.cr) + offset;
}
}
if (!jpeg_write_raw_data(cinfo, const_cast<JSAMPIMAGE>(planes), kJpegMCUSize)) {
return FAILURE(false);
}
}
return true;
}
// Since JPEG processes everything in blocks of kJpegMCUSize, we have to make
// both width and height a multiple of kJpegMCUSize. The height is handled by
// repeating the last line. compressYUVImplPixelsSlow handles the case when the
// image width is not a multiple of kJpegMCUSize by allocating a memory block
// large enough to hold kJpegMCUSize rows of the image with width aligned up to
// the next multiple of kJpegMCUSize. The original image has to be copied
// chunk-by-chunk into this memory block.
bool compressYUVImplPixelsSlow(const android_ycbcr& image, jpeg_compress_struct* cinfo,
const size_t alignedWidth, uint8_t* const alignedMemory) {
uint8_t* y[kJpegMCUSize];
uint8_t* cb[kJpegMCUSize / 2];
uint8_t* cr[kJpegMCUSize / 2];
uint8_t** planes[] = { y, cb, cr };
{
uint8_t* y0 = alignedMemory;
for (int i = 0; i < kJpegMCUSize; ++i, y0 += alignedWidth) {
y[i] = y0;
}
const size_t alignedWidth2 = alignedWidth / 2;
uint8_t* cb0 = y0;
uint8_t* cr0 = &cb0[kJpegMCUSize / 2 * alignedWidth2];
for (int i = 0; i < kJpegMCUSize / 2; ++i, cb0 += alignedWidth2, cr0 += alignedWidth2) {
cb[i] = cb0;
cr[i] = cr0;
}
}
const int width = cinfo->image_width;
const int width2 = width / 2;
const int height = cinfo->image_height;
const int height1 = height - 1;
const int ystride = image.ystride;
const int cstride = image.cstride;
while (true) {
const int nscl = cinfo->next_scanline;
if (nscl >= height) {
break;
}
for (int i = 0; i < kJpegMCUSize; ++i) {
const int nscli = std::min(nscl + i, height1);
memcpy(y[i], static_cast<const uint8_t*>(image.y) + nscli * ystride, width);
if ((i & 1) == 0) {
const int offset = (nscli / 2) * cstride;
memcpy(cb[i / 2], static_cast<const uint8_t*>(image.cb) + offset, width2);
memcpy(cr[i / 2], static_cast<const uint8_t*>(image.cr) + offset, width2);
}
}
if (!jpeg_write_raw_data(cinfo, const_cast<JSAMPIMAGE>(planes), kJpegMCUSize)) {
return FAILURE(false);
}
}
return true;
}
struct JpegErrorMgr : public jpeg_error_mgr {
JpegErrorMgr() {
error_exit = &onJpegErrorS;
}
void onJpegError(j_common_ptr cinfo) {
{
char errorMessage[JMSG_LENGTH_MAX];
memset(errorMessage, 0, sizeof(errorMessage));
(*format_message)(cinfo, errorMessage);
ALOGE("%s:%d: JPEG compression failed with '%s'",
__func__, __LINE__, errorMessage);
}
longjmp(jumpBuffer, 1);
}
static void onJpegErrorS(j_common_ptr cinfo) {
static_cast<JpegErrorMgr*>(cinfo->err)->onJpegError(cinfo);
}
jmp_buf jumpBuffer;
};
bool compressYUVImpl(const android_ycbcr& image, const Rect<uint16_t> imageSize,
unsigned char* const rawExif, const unsigned rawExifSize,
const int quality,
jpeg_destination_mgr* sink) {
if (image.chroma_step != 1) {
return FAILURE(false);
}
std::vector<uint8_t> alignedMemory;
jpeg_compress_struct cinfo;
JpegErrorMgr err;
bool result;
cinfo.err = jpeg_std_error(&err);
jpeg_create_compress(&cinfo);
cinfo.image_width = imageSize.width;
cinfo.image_height = imageSize.height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_YCbCr;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_default_colorspace(&cinfo);
cinfo.raw_data_in = TRUE;
cinfo.dct_method = JDCT_IFAST;
cinfo.comp_info[0].h_samp_factor = 2;
cinfo.comp_info[0].v_samp_factor = 2;
cinfo.comp_info[1].h_samp_factor = 1;
cinfo.comp_info[1].v_samp_factor = 1;
cinfo.comp_info[2].h_samp_factor = 1;
cinfo.comp_info[2].v_samp_factor = 1;
cinfo.dest = sink;
if (setjmp(err.jumpBuffer)) {
jpeg_destroy_compress(&cinfo);
return FAILURE(false);
}
jpeg_start_compress(&cinfo, TRUE);
if (rawExif) {
jpeg_write_marker(&cinfo, JPEG_APP0 + 1, rawExif, rawExifSize);
}
if (imageSize.width % kJpegMCUSize) {
const size_t alignedWidth =
((imageSize.width + kJpegMCUSize) / kJpegMCUSize) * kJpegMCUSize;
alignedMemory.resize(alignedWidth * kJpegMCUSize * 3 / 2);
result = compressYUVImplPixelsSlow(image, &cinfo, alignedWidth, alignedMemory.data());
} else {
result = compressYUVImplPixelsFast(image, &cinfo);
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
return result;
}
android_ycbcr resizeYUV(const android_ycbcr& srcYCbCr,
const Rect<uint16_t> srcSize,
const Rect<uint16_t> dstSize,
std::vector<uint8_t>* pDstData) {
if (srcYCbCr.chroma_step != 1) {
return FAILURE(android_ycbcr());
}
const size_t dstWidth = dstSize.width;
const size_t dstHeight = dstSize.height;
if ((dstWidth & 1) || (dstHeight & 1)) {
return FAILURE(android_ycbcr());
}
std::vector<uint8_t> dstData(yuv::NV21size(dstWidth, dstHeight));
const android_ycbcr dstYCbCr = yuv::NV21init(dstWidth, dstHeight, dstData.data());
const int result = libyuv::I420Scale(
static_cast<const uint8_t*>(srcYCbCr.y), srcYCbCr.ystride,
static_cast<const uint8_t*>(srcYCbCr.cb), srcYCbCr.cstride,
static_cast<const uint8_t*>(srcYCbCr.cr), srcYCbCr.cstride,
srcSize.width, srcSize.height,
static_cast<uint8_t*>(dstYCbCr.y), dstYCbCr.ystride,
static_cast<uint8_t*>(dstYCbCr.cb), dstYCbCr.cstride,
static_cast<uint8_t*>(dstYCbCr.cr), dstYCbCr.cstride,
dstWidth, dstHeight,
libyuv::kFilterBilinear);
if (result) {
return FAILURE_V(android_ycbcr(), "libyuv::I420Scale failed with %d", result);
} else {
*pDstData = std::move(dstData);
return dstYCbCr;
}
}
struct StaticBufferSink : public jpeg_destination_mgr {
StaticBufferSink(void* dst, const size_t dstCapacity) {
next_output_byte = static_cast<JOCTET*>(dst);
free_in_buffer = dstCapacity;
init_destination = &initDestinationS;
empty_output_buffer = &emptyOutputBufferS;
term_destination = &termDestinationS;
}
static void initDestinationS(j_compress_ptr) {}
static boolean emptyOutputBufferS(j_compress_ptr) { return 0; }
static void termDestinationS(j_compress_ptr) {}
};
constexpr int kDefaultQuality = 85;
int sanitizeJpegQuality(const int quality) {
if (quality <= 0) {
return kDefaultQuality;
} else if (quality > 100) {
return 100;
} else {
return quality;
}
}
} // namespace
size_t compressYUV(const android_ycbcr& image,
const Rect<uint16_t> imageSize,
const CameraMetadata& metadata,
void* const jpegData,
const size_t jpegDataCapacity) {
std::vector<uint8_t> nv21data;
const android_ycbcr imageNV21 =
yuv::toNV21Shallow(imageSize.width, imageSize.height,
image, &nv21data);
auto exifData = exif::createExifData(metadata, imageSize);
if (!exifData) {
return FAILURE(0);
}
const camera_metadata_t* const rawMetadata =
reinterpret_cast<const camera_metadata_t*>(metadata.metadata.data());
camera_metadata_ro_entry_t metadataEntry;
do {
Rect<uint16_t> thumbnailSize = {0, 0};
int thumbnailQuality = 0;
if (find_camera_metadata_ro_entry(rawMetadata, ANDROID_JPEG_THUMBNAIL_SIZE,
&metadataEntry)) {
break;
} else {
thumbnailSize.width = metadataEntry.data.i32[0];
thumbnailSize.height = metadataEntry.data.i32[1];
if ((thumbnailSize.width <= 0) || (thumbnailSize.height <= 0)) {
break;
}
}
if (find_camera_metadata_ro_entry(rawMetadata, ANDROID_JPEG_THUMBNAIL_QUALITY,
&metadataEntry)) {
thumbnailQuality = kDefaultQuality;
} else {
thumbnailQuality = sanitizeJpegQuality(metadataEntry.data.i32[0]);
}
std::vector<uint8_t> thumbnailData;
const android_ycbcr thumbmnail = resizeYUV(imageNV21, imageSize,
thumbnailSize, &thumbnailData);
if (!thumbmnail.y) {
return FAILURE(0);
}
StaticBufferSink sink(jpegData, jpegDataCapacity);
if (!compressYUVImpl(thumbmnail, thumbnailSize, nullptr, 0,
thumbnailQuality, &sink)) {
return FAILURE(0);
}
const size_t thumbnailJpegSize = jpegDataCapacity - sink.free_in_buffer;
void* exifThumbnailJpegDataPtr = exif::exifDataAllocThumbnail(
exifData.get(), thumbnailJpegSize);
if (!exifThumbnailJpegDataPtr) {
return FAILURE(0);
}
memcpy(exifThumbnailJpegDataPtr, jpegData, thumbnailJpegSize);
} while (false);
int quality;
if (find_camera_metadata_ro_entry(rawMetadata, ANDROID_JPEG_QUALITY,
&metadataEntry)) {
quality = kDefaultQuality;
} else {
quality = sanitizeJpegQuality(metadataEntry.data.i32[0]);
}
unsigned char* rawExif = nullptr;
unsigned rawExifSize = 0;
exif_data_save_data(const_cast<ExifData*>(exifData.get()),
&rawExif, &rawExifSize);
if (!rawExif) {
return FAILURE(0);
}
StaticBufferSink sink(jpegData, jpegDataCapacity);
const bool success = compressYUVImpl(imageNV21, imageSize, rawExif, rawExifSize,
quality, &sink);
free(rawExif);
return success ? (jpegDataCapacity - sink.free_in_buffer) : 0;
}
} // namespace jpeg
} // namespace implementation
} // namespace provider
} // namespace camera
} // namespace hardware
} // namespace android