383 lines
13 KiB
C++
383 lines
13 KiB
C++
/*
|
|
* 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
|