gecko-dev/third_party/jpeg-xl/lib/extras/codec_jpg.cc

530 строки
18 KiB
C++

// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "lib/extras/codec_jpg.h"
#include <stddef.h>
#include <stdio.h>
#if JPEGXL_ENABLE_JPEG
// After stddef/stdio
#include <jpeglib.h>
#include <setjmp.h>
#include <stdint.h>
#endif // JPEGXL_ENABLE_JPEG
#include <algorithm>
#include <iterator>
#include <numeric>
#include <utility>
#include <vector>
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/base/time.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/color_management.h"
#include "lib/jxl/common.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_bundle.h"
#include "lib/jxl/image_ops.h"
#include "lib/jxl/jpeg/dec_jpeg_data_writer.h"
#include "lib/jxl/jpeg/enc_jpeg_data.h"
#include "lib/jxl/jpeg/enc_jpeg_data_reader.h"
#include "lib/jxl/luminance.h"
#if JPEGXL_ENABLE_SJPEG
#include "sjpeg.h"
#endif
#ifdef MEMORY_SANITIZER
#include "sanitizer/msan_interface.h"
#endif
namespace jxl {
#if JPEGXL_ENABLE_JPEG
namespace {
constexpr float kJPEGSampleMultiplier = MAXJSAMPLE;
constexpr unsigned char kICCSignature[12] = {
0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00};
constexpr int kICCMarker = JPEG_APP0 + 2;
constexpr size_t kMaxBytesInMarker = 65533;
constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69,
0x66, 0x00, 0x00};
constexpr int kExifMarker = JPEG_APP0 + 1;
constexpr float kJPEGSampleMin = 0;
constexpr float kJPEGSampleMax = MAXJSAMPLE;
bool MarkerIsICC(const jpeg_saved_marker_ptr marker) {
return marker->marker == kICCMarker &&
marker->data_length >= sizeof kICCSignature + 2 &&
std::equal(std::begin(kICCSignature), std::end(kICCSignature),
marker->data);
}
bool MarkerIsExif(const jpeg_saved_marker_ptr marker) {
return marker->marker == kExifMarker &&
marker->data_length >= sizeof kExifSignature + 2 &&
std::equal(std::begin(kExifSignature), std::end(kExifSignature),
marker->data);
}
Status ReadICCProfile(jpeg_decompress_struct* const cinfo,
PaddedBytes* const icc) {
constexpr size_t kICCSignatureSize = sizeof kICCSignature;
// ICC signature + uint8_t index + uint8_t max_index.
constexpr size_t kICCHeadSize = kICCSignatureSize + 2;
// Markers are 1-indexed, and we keep them that way in this vector to get a
// convenient 0 at the front for when we compute the offsets later.
std::vector<size_t> marker_lengths;
int num_markers = 0;
int seen_markers_count = 0;
bool has_num_markers = false;
for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
marker = marker->next) {
#ifdef MEMORY_SANITIZER
// marker is initialized by libjpeg, which we are not instrumenting with
// msan.
__msan_unpoison(marker, sizeof(*marker));
__msan_unpoison(marker->data, marker->data_length);
#endif
if (!MarkerIsICC(marker)) continue;
const int current_marker = marker->data[kICCSignatureSize];
if (current_marker == 0) {
return JXL_FAILURE("inconsistent JPEG ICC marker numbering");
}
const int current_num_markers = marker->data[kICCSignatureSize + 1];
if (current_marker > current_num_markers) {
return JXL_FAILURE("inconsistent JPEG ICC marker numbering");
}
if (has_num_markers) {
if (current_num_markers != num_markers) {
return JXL_FAILURE("inconsistent numbers of JPEG ICC markers");
}
} else {
num_markers = current_num_markers;
has_num_markers = true;
marker_lengths.resize(num_markers + 1);
}
size_t marker_length = marker->data_length - kICCHeadSize;
if (marker_length == 0) {
// NB: if we allow empty chunks, then the next check is incorrect.
return JXL_FAILURE("Empty ICC chunk");
}
if (marker_lengths[current_marker] != 0) {
return JXL_FAILURE("duplicate JPEG ICC marker number");
}
marker_lengths[current_marker] = marker_length;
seen_markers_count++;
}
if (marker_lengths.empty()) {
// Not an error.
return false;
}
if (seen_markers_count != num_markers) {
JXL_DASSERT(has_num_markers);
return JXL_FAILURE("Incomplete set of ICC chunks");
}
std::vector<size_t> offsets = std::move(marker_lengths);
std::partial_sum(offsets.begin(), offsets.end(), offsets.begin());
icc->resize(offsets.back());
for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
marker = marker->next) {
if (!MarkerIsICC(marker)) continue;
const uint8_t* first = marker->data + kICCHeadSize;
uint8_t current_marker = marker->data[kICCSignatureSize];
size_t offset = offsets[current_marker - 1];
size_t marker_length = offsets[current_marker] - offset;
std::copy_n(first, marker_length, icc->data() + offset);
}
return true;
}
void ReadExif(jpeg_decompress_struct* const cinfo, PaddedBytes* const exif) {
constexpr size_t kExifSignatureSize = sizeof kExifSignature;
for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr;
marker = marker->next) {
#ifdef MEMORY_SANITIZER
// marker is initialized by libjpeg, which we are not instrumenting with
// msan.
__msan_unpoison(marker, sizeof(*marker));
__msan_unpoison(marker->data, marker->data_length);
#endif
if (!MarkerIsExif(marker)) continue;
size_t marker_length = marker->data_length - kExifSignatureSize;
exif->resize(marker_length);
std::copy_n(marker->data + kExifSignatureSize, marker_length, exif->data());
return;
}
}
// TODO (jon): take orientation into account when writing jpeg output
// TODO (jon): write Exif blob also in sjpeg encoding
// TODO (jon): overwrite orientation in Exif blob to avoid double orientation
void WriteICCProfile(jpeg_compress_struct* const cinfo,
const PaddedBytes& icc) {
constexpr size_t kMaxIccBytesInMarker =
kMaxBytesInMarker - sizeof kICCSignature - 2;
const int num_markers =
static_cast<int>(DivCeil(icc.size(), kMaxIccBytesInMarker));
size_t begin = 0;
for (int current_marker = 0; current_marker < num_markers; ++current_marker) {
const size_t length = std::min(kMaxIccBytesInMarker, icc.size() - begin);
jpeg_write_m_header(
cinfo, kICCMarker,
static_cast<unsigned int>(length + sizeof kICCSignature + 2));
for (const unsigned char c : kICCSignature) {
jpeg_write_m_byte(cinfo, c);
}
jpeg_write_m_byte(cinfo, current_marker + 1);
jpeg_write_m_byte(cinfo, num_markers);
for (size_t i = 0; i < length; ++i) {
jpeg_write_m_byte(cinfo, icc[begin]);
++begin;
}
}
}
void WriteExif(jpeg_compress_struct* const cinfo, const PaddedBytes& exif) {
if (exif.size() < 4) return;
jpeg_write_m_header(
cinfo, kExifMarker,
static_cast<unsigned int>(exif.size() - 4 + sizeof kExifSignature));
for (const unsigned char c : kExifSignature) {
jpeg_write_m_byte(cinfo, c);
}
for (size_t i = 4; i < exif.size(); ++i) {
jpeg_write_m_byte(cinfo, exif[i]);
}
}
Status SetChromaSubsampling(const YCbCrChromaSubsampling& chroma_subsampling,
jpeg_compress_struct* const cinfo) {
for (size_t i = 0; i < 3; i++) {
cinfo->comp_info[i].h_samp_factor =
1 << (chroma_subsampling.MaxHShift() -
chroma_subsampling.HShift(i < 2 ? i ^ 1 : i));
cinfo->comp_info[i].v_samp_factor =
1 << (chroma_subsampling.MaxVShift() -
chroma_subsampling.VShift(i < 2 ? i ^ 1 : i));
}
return true;
}
void MyErrorExit(j_common_ptr cinfo) {
jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data);
(*cinfo->err->output_message)(cinfo);
jpeg_destroy_decompress(reinterpret_cast<j_decompress_ptr>(cinfo));
longjmp(*env, 1);
}
void MyOutputMessage(j_common_ptr cinfo) {
#if JXL_DEBUG_WARNING == 1
char buf[JMSG_LENGTH_MAX];
(*cinfo->err->format_message)(cinfo, buf);
JXL_WARNING("%s", buf);
#endif
}
} // namespace
#endif // JPEGXL_ENABLE_JPEG
Status DecodeImageJPG(const Span<const uint8_t> bytes, ThreadPool* pool,
CodecInOut* io, double* const elapsed_deinterleave) {
if (elapsed_deinterleave != nullptr) *elapsed_deinterleave = 0;
// Don't do anything for non-JPEG files (no need to report an error)
if (!IsJPG(bytes)) return false;
const DecodeTarget target = io->dec_target;
// Use brunsli JPEG decoder to read quantized coefficients.
if (target == DecodeTarget::kQuantizedCoeffs) {
return jxl::jpeg::DecodeImageJPG(bytes, io);
}
#if JPEGXL_ENABLE_JPEG
// TODO(veluca): use JPEGData also for pixels?
// We need to declare all the non-trivial destructor local variables before
// the call to setjmp().
ColorEncoding color_encoding;
PaddedBytes icc;
Image3F image;
std::unique_ptr<JSAMPLE[]> row;
ImageBundle bundle(&io->metadata.m);
const auto try_catch_block = [&]() -> bool {
jpeg_decompress_struct cinfo;
#ifdef MEMORY_SANITIZER
// cinfo is initialized by libjpeg, which we are not instrumenting with
// msan, therefore we need to initialize cinfo here.
memset(&cinfo, 0, sizeof(cinfo));
#endif
// Setup error handling in jpeg library so we can deal with broken jpegs in
// the fuzzer.
jpeg_error_mgr jerr;
jmp_buf env;
cinfo.err = jpeg_std_error(&jerr);
jerr.error_exit = &MyErrorExit;
jerr.output_message = &MyOutputMessage;
if (setjmp(env)) {
return false;
}
cinfo.client_data = static_cast<void*>(&env);
jpeg_create_decompress(&cinfo);
jpeg_mem_src(&cinfo, reinterpret_cast<const unsigned char*>(bytes.data()),
bytes.size());
jpeg_save_markers(&cinfo, kICCMarker, 0xFFFF);
jpeg_save_markers(&cinfo, kExifMarker, 0xFFFF);
jpeg_read_header(&cinfo, TRUE);
if (!VerifyDimensions(&io->constraints, cinfo.image_width,
cinfo.image_height)) {
jpeg_abort_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return JXL_FAILURE("image too big");
}
if (ReadICCProfile(&cinfo, &icc)) {
if (!color_encoding.SetICC(std::move(icc))) {
jpeg_abort_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return JXL_FAILURE("read an invalid ICC profile");
}
} else {
color_encoding = ColorEncoding::SRGB(cinfo.output_components == 1);
}
ReadExif(&cinfo, &io->blobs.exif);
io->metadata.m.SetUintSamples(BITS_IN_JSAMPLE);
io->metadata.m.color_encoding = color_encoding;
int nbcomp = cinfo.num_components;
if (nbcomp != 1 && nbcomp != 3) {
jpeg_abort_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return JXL_FAILURE("unsupported number of components (%d) in JPEG",
cinfo.output_components);
}
(void)io->dec_hints.Foreach(
[](const std::string& key, const std::string& /*value*/) {
JXL_WARNING("JPEG decoder ignoring %s hint", key.c_str());
return true;
});
jpeg_start_decompress(&cinfo);
JXL_ASSERT(cinfo.output_components == nbcomp);
image = Image3F(cinfo.image_width, cinfo.image_height);
row.reset(new JSAMPLE[cinfo.output_components * cinfo.image_width]);
for (size_t y = 0; y < image.ysize(); ++y) {
JSAMPROW rows[] = {row.get()};
jpeg_read_scanlines(&cinfo, rows, 1);
#ifdef MEMORY_SANITIZER
__msan_unpoison(row.get(), sizeof(JSAMPLE) * cinfo.output_components *
cinfo.image_width);
#endif
auto start = Now();
float* const JXL_RESTRICT output_row[] = {
image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
if (cinfo.output_components == 1) {
for (size_t x = 0; x < image.xsize(); ++x) {
output_row[0][x] = output_row[1][x] = output_row[2][x] =
row[x] * (1.f / kJPEGSampleMultiplier);
}
} else { // 3 components
for (size_t x = 0; x < image.xsize(); ++x) {
for (size_t c = 0; c < 3; ++c) {
output_row[c][x] = row[3 * x + c] * (1.f / kJPEGSampleMultiplier);
}
}
}
auto end = Now();
if (elapsed_deinterleave != nullptr) {
*elapsed_deinterleave += end - start;
}
}
io->SetFromImage(std::move(image), color_encoding);
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
io->dec_pixels = io->xsize() * io->ysize();
return true;
};
return try_catch_block();
#else // JPEGXL_ENABLE_JPEG
return JXL_FAILURE("JPEG decoding not enabled at build time.");
#endif // JPEGXL_ENABLE_JPEG
}
#if JPEGXL_ENABLE_JPEG
Status EncodeWithLibJpeg(const ImageBundle* ib, const CodecInOut* io,
size_t quality,
const YCbCrChromaSubsampling& chroma_subsampling,
PaddedBytes* bytes) {
jpeg_compress_struct cinfo;
#ifdef MEMORY_SANITIZER
// cinfo is initialized by libjpeg, which we are not instrumenting with
// msan.
__msan_unpoison(&cinfo, sizeof(cinfo));
#endif
jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
unsigned char* buffer = nullptr;
unsigned long size = 0;
jpeg_mem_dest(&cinfo, &buffer, &size);
cinfo.image_width = ib->xsize();
cinfo.image_height = ib->ysize();
if (ib->IsGray()) {
cinfo.input_components = 1;
cinfo.in_color_space = JCS_GRAYSCALE;
} else {
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
}
jpeg_set_defaults(&cinfo);
cinfo.optimize_coding = TRUE;
if (cinfo.input_components == 3) {
JXL_RETURN_IF_ERROR(SetChromaSubsampling(chroma_subsampling, &cinfo));
}
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_start_compress(&cinfo, TRUE);
if (!ib->IsSRGB()) {
WriteICCProfile(&cinfo, ib->c_current().ICC());
}
WriteExif(&cinfo, io->blobs.exif);
if (cinfo.input_components > 3 || cinfo.input_components < 0)
return JXL_FAILURE("invalid numbers of components");
std::unique_ptr<JSAMPLE[]> row(
new JSAMPLE[cinfo.input_components * cinfo.image_width]);
for (size_t y = 0; y < ib->ysize(); ++y) {
const float* const JXL_RESTRICT input_row[3] = {
ib->color().ConstPlaneRow(0, y), ib->color().ConstPlaneRow(1, y),
ib->color().ConstPlaneRow(2, y)};
for (size_t x = 0; x < ib->xsize(); ++x) {
for (size_t c = 0; c < static_cast<size_t>(cinfo.input_components); ++c) {
JXL_RETURN_IF_ERROR(c < 3);
row[cinfo.input_components * x + c] = static_cast<JSAMPLE>(
std::max(std::min(kJPEGSampleMultiplier * input_row[c][x] + .5f,
kJPEGSampleMax),
kJPEGSampleMin));
}
}
JSAMPROW rows[] = {row.get()};
jpeg_write_scanlines(&cinfo, rows, 1);
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
bytes->resize(size);
#ifdef MEMORY_SANITIZER
// Compressed image data is initialized by libjpeg, which we are not
// instrumenting with msan.
__msan_unpoison(buffer, size);
#endif
std::copy_n(buffer, size, bytes->data());
std::free(buffer);
return true;
}
Status EncodeWithSJpeg(const ImageBundle* ib, size_t quality,
const YCbCrChromaSubsampling& chroma_subsampling,
PaddedBytes* bytes) {
#if !JPEGXL_ENABLE_SJPEG
return JXL_FAILURE("JPEG XL was built without sjpeg support");
#else
sjpeg::EncoderParam param(quality);
if (!ib->IsSRGB()) {
param.iccp.assign(ib->metadata()->color_encoding.ICC().begin(),
ib->metadata()->color_encoding.ICC().end());
}
if (chroma_subsampling.Is444()) {
param.yuv_mode = SJPEG_YUV_444;
} else if (chroma_subsampling.Is420()) {
param.yuv_mode = SJPEG_YUV_SHARP;
} else {
return JXL_FAILURE("sjpeg does not support this chroma subsampling mode");
}
std::vector<uint8_t> rgb;
rgb.reserve(ib->xsize() * ib->ysize() * 3);
for (size_t y = 0; y < ib->ysize(); ++y) {
const float* const rows[] = {
ib->color().ConstPlaneRow(0, y),
ib->color().ConstPlaneRow(1, y),
ib->color().ConstPlaneRow(2, y),
};
for (size_t x = 0; x < ib->xsize(); ++x) {
for (const float* const row : rows) {
rgb.push_back(static_cast<uint8_t>(
std::max(0.f, std::min(255.f, roundf(255.f * row[x])))));
}
}
}
std::string output;
JXL_RETURN_IF_ERROR(sjpeg::Encode(rgb.data(), ib->xsize(), ib->ysize(),
ib->xsize() * 3, param, &output));
bytes->assign(
reinterpret_cast<const uint8_t*>(output.data()),
reinterpret_cast<const uint8_t*>(output.data() + output.size()));
return true;
#endif
}
#endif // JPEGXL_ENABLE_JPEG
Status EncodeImageJPG(const CodecInOut* io, JpegEncoder encoder, size_t quality,
YCbCrChromaSubsampling chroma_subsampling,
ThreadPool* pool, PaddedBytes* bytes,
const DecodeTarget target) {
if (io->Main().HasAlpha()) {
return JXL_FAILURE("alpha is not supported");
}
if (quality > 100) {
return JXL_FAILURE("please specify a 0-100 JPEG quality");
}
if (target == DecodeTarget::kQuantizedCoeffs) {
auto write = [&bytes](const uint8_t* buf, size_t len) {
bytes->append(buf, buf + len);
return len;
};
return jpeg::WriteJpeg(*io->Main().jpeg_data, write);
}
#if JPEGXL_ENABLE_JPEG
const ImageBundle* ib;
ImageMetadata metadata = io->metadata.m;
ImageBundle ib_store(&metadata);
JXL_RETURN_IF_ERROR(TransformIfNeeded(
io->Main(), io->metadata.m.color_encoding, pool, &ib_store, &ib));
switch (encoder) {
case JpegEncoder::kLibJpeg:
JXL_RETURN_IF_ERROR(
EncodeWithLibJpeg(ib, io, quality, chroma_subsampling, bytes));
break;
case JpegEncoder::kSJpeg:
JXL_RETURN_IF_ERROR(
EncodeWithSJpeg(ib, quality, chroma_subsampling, bytes));
break;
default:
return JXL_FAILURE("tried to use an unknown JPEG encoder");
}
return true;
#else // JPEGXL_ENABLE_JPEG
return JXL_FAILURE("JPEG pixel encoding not enabled at build time");
#endif // JPEGXL_ENABLE_JPEG
}
} // namespace jxl