| // Copyright 2019 Google LLC |
| // |
| // 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 |
| // |
| // https://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. |
| // ----------------------------------------------------------------------------- |
| // |
| // JPEG decode. |
| // |
| // Author: Skal (pascal.massimino@gmail.com) |
| |
| #include <cstdio> |
| #include <cstdlib> |
| |
| #ifdef HAVE_CONFIG_H |
| #include "src/wp2/config.h" |
| #endif |
| #include "imageio/anim_image_dec.h" |
| #include "imageio/imageio_util.h" |
| #include "src/wp2/base.h" |
| |
| #ifdef WP2_HAVE_JPEG |
| #include <jerror.h> |
| #include <jpeglib.h> |
| |
| #include <cassert> |
| #include <csetjmp> |
| #include <cstdint> |
| #include <cstring> |
| |
| #include "src/utils/utils.h" |
| |
| namespace { |
| |
| constexpr int kNumOutputComponents = 3; |
| |
| // ----------------------------------------------------------------------------- |
| // Metadata processing |
| |
| // JPEG application-specific markers used for EXIF, XMP and ICCP. |
| // See https://en.wikipedia.org/wiki/JPEG |
| #ifndef JPEG_APP1 |
| #define JPEG_APP1 (JPEG_APP0 + 1) |
| #endif |
| #ifndef JPEG_APP2 |
| #define JPEG_APP2 (JPEG_APP0 + 2) |
| #endif |
| |
| // Struct used for storing a JPEG metadata signature. |
| struct MetadataSignature { |
| int code; // JPEG application-specific marker. |
| const uint8_t* bytes; // Signature. |
| size_t size; // Signature length. |
| size_t skip_size; // Signature length plus some header bytes. |
| }; |
| |
| // Exif 2.2 Section 4.7.2 Interoperability Structure of APP1 ... |
| const MetadataSignature kEXIFSignature = {JPEG_APP1, (const uint8_t*)"Exif\0", |
| 6, 6}; |
| |
| // XMP Specification Part 3 Section 3 Embedding XMP Metadata ... #JPEG |
| // TODO(jzern) Add support for 'ExtendedXMP' |
| const MetadataSignature kXMPSignature = { |
| JPEG_APP1, (const uint8_t*)"http://ns.adobe.com/xap/1.0/", 29, 29}; |
| |
| // ICC.1:2010-12 (4.3.0.0) Annex B.4 Embedding ICC Profiles in JPEG files |
| const MetadataSignature kICCPSignature = { |
| JPEG_APP2, (const uint8_t*)"ICC_PROFILE", 12, 14}; |
| |
| void SetSaveMarkers(struct jpeg_decompress_struct* const dinfo) { |
| const uint32_t max_marker_length = 0xffffu; |
| jpeg_save_markers(dinfo, JPEG_APP1, max_marker_length); // Exif/XMP |
| jpeg_save_markers(dinfo, JPEG_APP2, max_marker_length); // ICC profile |
| } |
| |
| #undef JPEG_APP1 |
| #undef JPEG_APP2 |
| |
| // Returns true if 'marker_code' and 'marker_data' match the 'signature'. |
| bool MarkerHasSignature(int marker_code, WP2::DataView marker_data, |
| const MetadataSignature& signature) { |
| return (marker_code == signature.code && |
| marker_data.size >= signature.skip_size && |
| !std::memcmp(marker_data.bytes, signature.bytes, signature.size)); |
| } |
| |
| // Extracts chunk from 'marker_data' to 'metadata'. |
| WP2Status ExtractMetadataFromMarker(WP2::DataView marker_data, |
| const MetadataSignature& signature, |
| WP2::Data* const metadata, |
| WP2::LogLevel log_level) { |
| if (!metadata->IsEmpty()) { |
| if (log_level >= WP2::LogLevel::VERBOSE) { |
| fprintf(stderr, "Ignoring additional '%s' marker\n", signature.bytes); |
| } |
| return WP2_STATUS_OK; |
| } |
| assert(marker_data.size >= signature.size); // Tested in MarkerHasSignature() |
| return metadata->CopyFrom(marker_data.bytes + signature.size, |
| marker_data.size - signature.size); |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // ICCP metadata processing |
| |
| struct ICCPSegment { |
| const uint8_t* data; |
| size_t data_length; |
| size_t seq; // Sequence number [1, 255] for use in reassembly. |
| }; |
| |
| // Extracts ICC profile segment from 'marker_data' to 'iccp_segments'. |
| // If 'iccp_segment_expected_count' is 0, it will be set to the segment's |
| // extracted count. Otherwise it will be compared to it. |
| // 'iccp_max_seq' will be set to the segment's extracted sequence number if it's |
| // the highest seen so far. |
| // Returns WP2_STATUS_OK on success. |
| WP2Status StoreICCPFromMarker(WP2::DataView marker_data, |
| ICCPSegment iccp_segments[255], |
| size_t* const iccp_segment_expected_count, |
| size_t* const iccp_max_seq, |
| WP2::LogLevel log_level) { |
| // ICC_PROFILE\0<seq><count>; 'seq' starts at 1. |
| const uint32_t seq = marker_data.bytes[kICCPSignature.size]; |
| const uint32_t count = marker_data.bytes[kICCPSignature.size + 1]; |
| const size_t segment_size = marker_data.size - kICCPSignature.skip_size; |
| |
| if (segment_size == 0) { |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, "[ICCP] size (%u) cannot be 0!\n", |
| (uint32_t)segment_size); |
| } |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } else if (count == 0) { |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, "[ICCP] count (%u) cannot be 0!\n", count); |
| } |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } else if (seq == 0) { |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, "[ICCP] invalid sequence (%u)!\n", seq); |
| } |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } |
| |
| if (*iccp_segment_expected_count == 0) { |
| *iccp_segment_expected_count = count; |
| } else if (*iccp_segment_expected_count != count) { |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, "[ICCP] Inconsistent segment count (%u / %u)!\n", |
| (uint32_t)*iccp_segment_expected_count, count); |
| } |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } |
| |
| ICCPSegment* const segment = iccp_segments + seq - 1; |
| if (segment->data_length != 0) { |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, "[ICCP] Duplicate segment number (%u)!\n", seq); |
| } |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } |
| |
| segment->data = marker_data.bytes + kICCPSignature.skip_size; |
| segment->data_length = segment_size; |
| segment->seq = seq; |
| if (seq > *iccp_max_seq) *iccp_max_seq = seq; |
| return WP2_STATUS_OK; |
| } |
| |
| // Returns WP2_STATUS_OK if everything's fine. |
| WP2Status VerifyICCP(size_t iccp_segment_count, |
| size_t iccp_segment_expected_count, size_t iccp_max_seq, |
| WP2::LogLevel log_level) { |
| if (iccp_segment_expected_count != iccp_segment_count) { |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, "[ICCP] Segment count: %u does not match expected: %u!\n", |
| (uint32_t)iccp_segment_count, |
| (uint32_t)iccp_segment_expected_count); |
| } |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } |
| if (iccp_max_seq != iccp_segment_count) { |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, |
| "[ICCP] Discontinuous segments, expected: %u actual: %u!\n", |
| (uint32_t)iccp_segment_count, (uint32_t)iccp_max_seq); |
| } |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } |
| return WP2_STATUS_OK; |
| } |
| |
| // Merges all ICC profile segments from 'iccp_segments' to 'metadata'. |
| // Returns WP2_STATUS_OK on success. |
| WP2Status MergeICCP(ICCPSegment iccp_segments[255], size_t iccp_segment_count, |
| WP2::Data* const metadata) { |
| // The segments may appear out of order in the file, sort them based on |
| // sequence number before assembling the payload. |
| qsort(iccp_segments, iccp_segment_count, sizeof(*iccp_segments), |
| [](const void* a, const void* b) { |
| const size_t seq_a = ((const ICCPSegment*)a)->seq; |
| const size_t seq_b = ((const ICCPSegment*)b)->seq; |
| return (seq_a < seq_b) ? -1 : (seq_a > seq_b) ? 1 : 0; |
| }); |
| |
| size_t iccp_total_size = 0; |
| for (size_t i = 0; i < iccp_segment_count; ++i) { |
| iccp_total_size += iccp_segments[i].data_length; |
| } |
| WP2_CHECK_STATUS(metadata->Resize(iccp_total_size, /*keep_bytes=*/false)); |
| |
| uint8_t* dst = metadata->bytes; |
| for (size_t i = 0; i < iccp_segment_count; ++i) { |
| memcpy(dst, iccp_segments[i].data, iccp_segments[i].data_length); |
| dst += iccp_segments[i].data_length; |
| } |
| return WP2_STATUS_OK; |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // Metadata processing |
| |
| WP2Status ExtractMetadataFromJPEG(j_decompress_ptr dinfo, |
| WP2::Metadata* const metadata, |
| WP2::LogLevel log_level) { |
| metadata->Clear(); |
| |
| // Treat ICC profiles separately as they may be segmented and out of order. |
| size_t iccp_segment_count = 0; |
| size_t iccp_segment_expected_count = 0; |
| size_t iccp_max_seq = 0; |
| ICCPSegment iccp_segments[255]; |
| memset(iccp_segments, 0, sizeof(iccp_segments)); |
| |
| jpeg_saved_marker_ptr marker; |
| for (marker = dinfo->marker_list; marker != NULL; marker = marker->next) { |
| const int marker_code = marker->marker; |
| const WP2::DataView marker_data = {marker->data, marker->data_length}; |
| if (MarkerHasSignature(marker_code, marker_data, kEXIFSignature)) { |
| WP2_CHECK_STATUS(ExtractMetadataFromMarker(marker_data, kEXIFSignature, |
| &metadata->exif, log_level)); |
| } else if (MarkerHasSignature(marker_code, marker_data, kXMPSignature)) { |
| WP2_CHECK_STATUS(ExtractMetadataFromMarker(marker_data, kXMPSignature, |
| &metadata->xmp, log_level)); |
| } else if (MarkerHasSignature(marker_code, marker_data, kICCPSignature)) { |
| WP2_CHECK_STATUS(StoreICCPFromMarker(marker_data, iccp_segments, |
| &iccp_segment_expected_count, |
| &iccp_max_seq, log_level)); |
| ++iccp_segment_count; |
| } else if (log_level >= WP2::LogLevel::VERBOSE) { |
| fprintf(stderr, "Ignoring marker '%d'\n", marker->marker); |
| } |
| } |
| |
| if (iccp_segment_count > 0) { |
| WP2_CHECK_STATUS(VerifyICCP(iccp_segment_count, iccp_segment_expected_count, |
| iccp_max_seq, log_level)); |
| WP2_CHECK_STATUS( |
| MergeICCP(iccp_segments, iccp_segment_count, &metadata->iccp)); |
| } |
| return WP2_STATUS_OK; |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // Error handling |
| |
| struct JPEGErrorContext { |
| struct jpeg_error_mgr pub; |
| jmp_buf setjmp_buffer; |
| }; |
| |
| void ErrorExitFunctionNoLog(j_common_ptr dinfo) { |
| JPEGErrorContext* const myerr = (JPEGErrorContext*)dinfo->err; |
| longjmp(myerr->setjmp_buffer, 1); |
| } |
| |
| void ErrorExitFunction(j_common_ptr dinfo) { |
| dinfo->err->output_message(dinfo); |
| ErrorExitFunctionNoLog(dinfo); |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // Context |
| |
| struct JPEGReadContext { |
| struct jpeg_source_mgr pub; |
| const uint8_t* data; |
| size_t data_size; |
| }; |
| |
| void ContextInit(j_decompress_ptr dinfo) { |
| JPEGReadContext* const ctx = (JPEGReadContext*)dinfo->src; |
| ctx->pub.next_input_byte = ctx->data; |
| ctx->pub.bytes_in_buffer = ctx->data_size; |
| } |
| |
| boolean ContextFill(j_decompress_ptr dinfo) { |
| // We shouldn't get here. |
| ERREXIT(dinfo, JERR_FILE_READ); |
| return FALSE; |
| } |
| |
| void ContextSkip(j_decompress_ptr dinfo, long jump_size) { // NOLINT (long) |
| JPEGReadContext* const ctx = (JPEGReadContext*)dinfo->src; |
| size_t jump = (size_t)jump_size; |
| if (jump > ctx->pub.bytes_in_buffer) { // Don't overflow the buffer. |
| jump = ctx->pub.bytes_in_buffer; |
| } |
| ctx->pub.bytes_in_buffer -= jump; |
| ctx->pub.next_input_byte += jump; |
| } |
| |
| void ContextTerm(j_decompress_ptr dinfo) { (void)dinfo; } |
| |
| void ContextSetup(j_decompress_ptr dinfo, JPEGReadContext* const ctx) { |
| assert((void*)ctx == (void*)&ctx->pub); |
| dinfo->src = &ctx->pub; |
| ctx->pub.init_source = ContextInit; |
| ctx->pub.fill_input_buffer = ContextFill; |
| ctx->pub.skip_input_data = ContextSkip; |
| ctx->pub.resync_to_restart = jpeg_resync_to_restart; |
| ctx->pub.term_source = ContextTerm; |
| ctx->pub.bytes_in_buffer = 0; |
| ctx->pub.next_input_byte = NULL; |
| } |
| |
| } // namespace |
| |
| // ----------------------------------------------------------------------------- |
| |
| namespace WP2 { |
| |
| class ImageReaderJPEG : public ImageReader::Impl { |
| public: |
| ImageReaderJPEG(const uint8_t* data, size_t data_size, |
| ArgbBuffer* const buffer, LogLevel log_level, |
| size_t max_num_pixels) |
| : ImageReader::Impl(buffer, data, data_size, log_level, max_num_pixels) {} |
| |
| WP2Status ReadFrame(bool* const is_last, |
| uint32_t* const duration_ms) override { |
| WP2_CHECK_STATUS(CheckData()); |
| |
| // The following are volatile because they need to keep their value for |
| // deletion in case of longjmp() error handling. |
| volatile jpeg_decompress_struct dinfo; |
| memset((j_decompress_ptr)&dinfo, 0, sizeof(dinfo)); // for setjmp sanity |
| uint8_t* volatile tmp_rgb = nullptr; |
| |
| JPEGReadContext ctx; |
| memset(&ctx, 0, sizeof(ctx)); |
| ctx.data = data_; |
| ctx.data_size = data_size_; |
| |
| JPEGErrorContext jerr; |
| memset(&jerr, 0, sizeof(jerr)); |
| dinfo.err = jpeg_std_error(&jerr.pub); |
| jerr.pub.error_exit = (log_level_ >= LogLevel::DEFAULT) |
| ? ErrorExitFunction |
| : ErrorExitFunctionNoLog; |
| |
| if (setjmp(jerr.setjmp_buffer)) { |
| // On a libjpeg error, error_exit() is called, longjmping here. |
| jpeg_destroy_decompress((j_decompress_ptr)&dinfo); |
| WP2Free(tmp_rgb); |
| return WP2_STATUS_BITSTREAM_ERROR; |
| } |
| |
| jpeg_create_decompress((j_decompress_ptr)&dinfo); |
| |
| // Avoid out-of-memory issues. |
| if (MultFitsIn(max_num_pixels_, kNumOutputComponents, |
| &dinfo.mem->max_memory_to_use) != WP2_STATUS_OK) { |
| // If 'max_num_pixels_ * kNumOutputComponents' does not fit in the |
| // 'max_memory_to_use' variable, leave it to its default value so memory |
| // is not limited to a lower-than-specified bound. |
| } |
| SetSaveMarkers((j_decompress_ptr)&dinfo); |
| ContextSetup((j_decompress_ptr)&dinfo, &ctx); |
| |
| jpeg_read_header((j_decompress_ptr)&dinfo, TRUE); |
| |
| dinfo.out_color_space = JCS_RGB; |
| dinfo.do_fancy_upsampling = TRUE; |
| |
| WP2Status status = ReadCanvas(&dinfo, &tmp_rgb); |
| WP2Free(tmp_rgb); |
| tmp_rgb = nullptr; |
| |
| if (status == WP2_STATUS_OK) { |
| status = ExtractMetadataFromJPEG((j_decompress_ptr)&dinfo, |
| &buffer_->metadata_, log_level_); |
| } |
| |
| if (status == WP2_STATUS_OK) { |
| jpeg_finish_decompress((j_decompress_ptr)&dinfo); |
| } |
| jpeg_destroy_decompress((j_decompress_ptr)&dinfo); |
| if (status == WP2_STATUS_OK) { |
| *is_last = true; |
| *duration_ms = ImageReader::kInfiniteDuration; |
| } |
| return status; |
| } |
| |
| protected: |
| // Reads pixels into 'buffer_'. |
| // Allocates '*tmp_rgb' which is volatile because allocation happens between |
| // setjmp() and longjmp() and needs to be freed afterwards. |
| WP2Status ReadCanvas(volatile jpeg_decompress_struct* const dinfo, |
| uint8_t* volatile* const tmp_rgb) { |
| // Use jpeg_calc_output_dimensions() to obtain the image dimensions before |
| // calling jpeg_start_decompress() which can be slow. |
| jpeg_calc_output_dimensions((j_decompress_ptr)dinfo); |
| |
| WP2_CHECK_OK(dinfo->output_components == kNumOutputComponents, |
| WP2_STATUS_UNSUPPORTED_FEATURE); |
| |
| const uint32_t width = dinfo->output_width; |
| const uint32_t height = dinfo->output_height; |
| WP2_CHECK_STATUS(CheckDimensions(width, height)); |
| |
| const int depth = dinfo->output_components * sizeof(**tmp_rgb); |
| size_t stride = 0; |
| WP2_CHECK_STATUS(MultFitsIn(width, depth, &stride)); |
| // Make sure whole size fits in size_t. |
| WP2_CHECK_STATUS(MultFitsIn<size_t>(stride, height)); |
| |
| WP2_CHECK_STATUS(buffer_->Resize(width, height)); |
| |
| *tmp_rgb = (uint8_t*)WP2Malloc(1, stride); |
| WP2_CHECK_ALLOC_OK(*tmp_rgb != nullptr); |
| JSAMPROW buffer[1]; |
| buffer[0] = (JSAMPLE*)*tmp_rgb; |
| |
| jpeg_start_decompress((j_decompress_ptr)dinfo); |
| |
| while (dinfo->output_scanline < dinfo->output_height) { |
| const uint32_t row = dinfo->output_scanline; |
| WP2_CHECK_OK(jpeg_read_scanlines((j_decompress_ptr)dinfo, buffer, 1) == 1, |
| WP2_STATUS_BITSTREAM_ERROR); |
| WP2_CHECK_STATUS(buffer_->ImportRow(WP2_RGB_24, row, *tmp_rgb)); |
| } |
| return WP2_STATUS_OK; |
| } |
| }; |
| |
| // ----------------------------------------------------------------------------- |
| |
| void ImageReader::SetImplJPEG(ArgbBuffer* const buffer, LogLevel log_level, |
| size_t max_num_pixels) { |
| impl_.reset(new (WP2Allocable::nothrow) ImageReaderJPEG( |
| data_.bytes, data_.size, buffer, log_level, max_num_pixels)); |
| if (impl_ == nullptr) status_ = WP2_STATUS_OUT_OF_MEMORY; |
| } |
| |
| } // namespace WP2 |
| |
| #else // !WP2_HAVE_JPEG |
| |
| void WP2::ImageReader::SetImplJPEG(WP2::ArgbBuffer* const buffer, |
| WP2::LogLevel log_level, |
| size_t max_num_pixels) { |
| (void)buffer; |
| (void)max_num_pixels; |
| if (log_level >= WP2::LogLevel::DEFAULT) { |
| fprintf(stderr, |
| "JPEG support not compiled. Please install the libjpeg " |
| "development package before building.\n"); |
| } |
| status_ = WP2_STATUS_UNSUPPORTED_FEATURE; |
| } |
| |
| #endif // WP2_HAVE_JPEG |