| // Copyright 2018 The Cobalt Authors. All Rights Reserved. |
| // |
| // 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. |
| |
| #include "starboard/shared/libjpeg/jpeg_image_decoder.h" |
| |
| #include <setjmp.h> |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "starboard/common/log.h" |
| #include "starboard/configuration.h" |
| #include "starboard/linux/shared/decode_target_internal.h" |
| |
| // Inhibit C++ name-mangling for libjpeg functions. |
| extern "C" { |
| #include "third_party/libjpeg/jpeglib.h" |
| #include "third_party/libjpeg/jpegint.h" |
| } |
| |
| namespace starboard { |
| namespace shared { |
| namespace libjpeg { |
| |
| namespace { |
| |
| JDIMENSION kInvalidHeight = 0xFFFFFF; |
| |
| void ErrorManagerExit(j_common_ptr common_ptr) { |
| // Returns the control to the setjmp point. The buffer which is filled by a |
| // previous call to setjmp that contains information to store the environment |
| // to that point. |
| jmp_buf* buffer = static_cast<jmp_buf*>(common_ptr->client_data); |
| longjmp(*buffer, 1); |
| } |
| |
| void SourceManagerInitSource(j_decompress_ptr decompress_ptr) { |
| SB_UNREFERENCED_PARAMETER(decompress_ptr); |
| // no-op. |
| } |
| |
| boolean SourceManagerFillInputBuffer(j_decompress_ptr decompress_ptr) { |
| SB_UNREFERENCED_PARAMETER(decompress_ptr); |
| // Normally, this function is called when we need to read more of the encoded |
| // buffer into memory and a return false indicates that we have no data to |
| // supply yet, but in our case, the encoded buffer is always in memory, so |
| // this should never get called unless there is an error, in which case we |
| // return false to indicate that. |
| return false; |
| } |
| |
| boolean SourceManagerResyncToRestart(j_decompress_ptr decompress_ptr, |
| int desired) { |
| SB_UNREFERENCED_PARAMETER(decompress_ptr); |
| SB_UNREFERENCED_PARAMETER(desired); |
| return false; |
| } |
| |
| void SourceManagerTermSource(j_decompress_ptr decompress_ptr) { |
| SB_UNREFERENCED_PARAMETER(decompress_ptr); |
| // no-op. |
| } |
| |
| void SourceManagerSkipInputData(j_decompress_ptr decompress_ptr, |
| long num_bytes) { // NOLINT(runtime/int) |
| // Since all our data that is required to be decoded should be in the buffer |
| // already, trying to skip beyond it means that there is some kind of error or |
| // corrupt input data. We acknowledge these error cases by setting |
| // |bytes_in_buffer| to 0 which will result in a call to |
| // SourceManagerFillInputBuffer() which will return false to indicate that an |
| // error has occurred. |
| size_t bytes_to_skip = std::min(decompress_ptr->src->bytes_in_buffer, |
| static_cast<size_t>(num_bytes)); |
| decompress_ptr->src->bytes_in_buffer -= bytes_to_skip; |
| decompress_ptr->src->next_input_byte += bytes_to_skip; |
| } |
| |
| bool ReadHeader(jpeg_decompress_struct* info) { |
| if (jpeg_read_header(info, true) != JPEG_HEADER_OK) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool StartDecompress(jpeg_decompress_struct* info) { |
| // jpeg_has_multiple_scans() returns TRUE if the incoming image file has more |
| // than one scan. |
| info->buffered_image = jpeg_has_multiple_scans(info); |
| |
| // TODO: switch libjpeg version to support JCS_RGBA_8888 output. |
| info->out_color_space = JCS_RGB; |
| |
| // Compute output image dimensions |
| jpeg_calc_output_dimensions(info); |
| |
| if (!jpeg_start_decompress(info)) { |
| SB_LOG(WARNING) << "Start decompressor failed."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Responsible for swizzeling and alpha-premultiplying a row of pixels. |
| template <int r, int g, int b, int a> |
| void FillRow(int width, uint8_t* dest, JSAMPLE* source) { |
| for (int x = 0; x < width; ++x, source += 3, dest += 4) { |
| dest[r] = source[0]; |
| dest[g] = source[1]; |
| dest[b] = source[2]; |
| dest[a] = 0xFF; |
| } |
| } |
| |
| bool ReadLines(jpeg_decompress_struct* info, |
| SbDecodeTargetFormat decode_target_format, |
| uint8_t* image_data) { |
| // Creation of 2-D sample arrays which is for one row. |
| // See the comments in jmemmgr.c. |
| JSAMPARRAY buffer = (*info->mem->alloc_sarray)( |
| (j_common_ptr)info, JPOOL_IMAGE, info->output_width * 4, 1); |
| |
| while (info->output_scanline < info->output_height) { |
| // |info->output_scanline| would be increased 1 after reading one row. |
| int row_index = static_cast<int>(info->output_scanline); |
| |
| // jpeg_read_scanlines() returns up to the maximum number of scanlines of |
| // decompressed image data. This may be less than the number requested in |
| // cases such as bottom of image, data source suspension, and operating |
| // modes that emit multiple scanlines at a time. Image data should be |
| // returned in top-to-bottom scanline order. |
| // TODO: Investigate the performance improvements by processing |
| // multiple pixel rows. It may have performance advantage to use values |
| // larger than 1. For example, JPEG images often use 4:2:0 downsampling, and |
| // in that case libjpeg needs to make an additional copy of half the image |
| // pixels(see merged_2v_upsample()). |
| if (jpeg_read_scanlines(info, buffer, 1) != 1) { |
| return false; |
| } |
| |
| // Write the decoded row pixels to image data. |
| uint8_t* pixel_data = image_data + info->image_width * 4 * row_index; |
| |
| JSAMPLE* sample_buffer = *buffer; |
| switch (decode_target_format) { |
| case kSbDecodeTargetFormat1PlaneRGBA: |
| FillRow<0, 1, 2, 3>(static_cast<int>(info->image_width), pixel_data, |
| sample_buffer); |
| break; |
| case kSbDecodeTargetFormat1PlaneBGRA: |
| FillRow<2, 1, 0, 3>(static_cast<int>(info->image_width), pixel_data, |
| sample_buffer); |
| break; |
| case kSbDecodeTargetFormat2PlaneYUVNV12: |
| case kSbDecodeTargetFormat3PlaneYUVI420: |
| #if SB_API_VERSION >= 10 |
| case kSbDecodeTargetFormat3Plane10BitYUVI420: |
| #endif // SB_API_VERSION >= 10 |
| case kSbDecodeTargetFormat1PlaneUYVY: |
| case kSbDecodeTargetFormatInvalid: |
| SB_NOTREACHED(); |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| // A baseline sequential JPEG is stored as one top-to-bottom scan of the image. |
| // Progressive JPEG divides the file into a series of scans. It is starting with |
| // a very low quality image, and then following scans gradually improve the |
| // quality. |
| // TODO: support displaying the low resolution image while decoding |
| // the progressive JPEG. |
| bool DecodeProgressiveJPEG(jpeg_decompress_struct* info, |
| SbDecodeTargetFormat decode_target_format, |
| uint8_t* image_data) { |
| int status; |
| do { |
| // |jpeg_consume_input| decodes the input data as it arrives. |
| status = jpeg_consume_input(info); |
| } while (status == JPEG_REACHED_SOS || status == JPEG_ROW_COMPLETED || |
| status == JPEG_SCAN_COMPLETED); |
| |
| while (info->output_scanline == kInvalidHeight || |
| info->output_scanline <= info->output_height) { |
| if (info->output_scanline == 0) { |
| // Initialize for an output pass in buffered-image mode. |
| // The |input_scan_number| indicates the scan of the image to be |
| // processed. |
| if (!jpeg_start_output(info, info->input_scan_number)) { |
| // Decompression is suspended. |
| return false; |
| } |
| } |
| |
| if (info->output_scanline == kInvalidHeight) { |
| // Recover from the previous set flag. |
| info->output_scanline = 0; |
| } |
| |
| if (!ReadLines(info, decode_target_format, image_data)) { |
| if (info->output_scanline == 0) { |
| // Data is not enough for one line scan, so flag the |output_scanline| |
| // to make sure that we don't call |jpeg_start_output| multiple times |
| // for the same scan. |
| info->output_scanline = kInvalidHeight; |
| } |
| return false; |
| } |
| |
| if (info->output_scanline == info->output_height) { |
| // Finish up after an output pass in buffered-image mode. |
| if (!jpeg_finish_output(info)) { |
| // Decompression is suspended due to |
| // input_scan_number <= output_scan_number and EOI is not reached. |
| // The suspension will be recovered when more data are coming. |
| return false; |
| } |
| |
| // The output scan number is the notional scan being processed by the |
| // output side. The decompressor will not allow output scan number to get |
| // ahead of input scan number. |
| SB_DCHECK(info->input_scan_number >= info->output_scan_number); |
| // This scan pass is done, so reset the output scanline. |
| info->output_scanline = 0; |
| if (info->input_scan_number == info->output_scan_number) { |
| // No more scan needed at this point. |
| break; |
| } |
| } |
| } |
| |
| // |jpeg_input_complete| tests for the end of image. |
| if (!jpeg_input_complete(info)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| SbDecodeTarget Decode(SbDecodeTargetGraphicsContextProvider* context_provider, |
| SbDecodeTargetFormat decode_target_format, |
| const uint8_t* data, |
| size_t input_byte) { |
| jpeg_decompress_struct info = {}; |
| jpeg_source_mgr source_manager = {}; |
| jpeg_error_mgr error_manager = {}; |
| |
| info.err = jpeg_std_error(&error_manager); |
| error_manager.error_exit = &ErrorManagerExit; |
| |
| // Allocate and initialize JPEG decompression object. |
| jpeg_create_decompress(&info); |
| |
| info.src = &source_manager; |
| |
| // Set up callback functions. |
| // Even some of them are no-op, we have to set them up for jpeg lib. |
| source_manager.init_source = SourceManagerInitSource; |
| source_manager.fill_input_buffer = SourceManagerFillInputBuffer; |
| source_manager.skip_input_data = SourceManagerSkipInputData; |
| source_manager.resync_to_restart = SourceManagerResyncToRestart; |
| source_manager.term_source = SourceManagerTermSource; |
| |
| // |client_data| is available for use by application. |
| jmp_buf jump_buffer; |
| info.client_data = &jump_buffer; |
| |
| // int setjmp(jmp_buf env) saves the current environment (ths program state), |
| // at some point of program execution, into a platform-specific data |
| // structure (jmp_buf) that can be used at some later point of program |
| // execution by longjmp to restore the program state to that saved by setjmp |
| // into jmp_buf. This process can be imagined to be a "jump" back to the point |
| // of program execution where setjmp saved the environment. The return value |
| // from setjmp indicates whether control reached that point normally or from a |
| // call to longjmp. If the return is from a direct invocation, setjmp returns |
| // 0. If the return is from a call to longjmp, setjmp returns a nonzero value. |
| // warning C4611: interaction between '_setjmp' and C++ object destruction is |
| // non-portable. |
| if (setjmp(jump_buffer)) { |
| // image data is empty. |
| SB_DLOG(WARNING) << "Decoder encounters an error."; |
| return kSbDecodeTargetInvalid; |
| } |
| |
| // Next byte to read from buffer. |
| info.src->next_input_byte = reinterpret_cast<const JOCTET*>(data); |
| // Number of bytes remaining in buffer. |
| info.src->bytes_in_buffer = input_byte; |
| |
| if (!ReadHeader(&info)) { |
| // Data is not enough for decoding the header. |
| return kSbDecodeTargetInvalid; |
| } |
| |
| if (!StartDecompress(&info)) { |
| return kSbDecodeTargetInvalid; |
| } |
| |
| std::vector<uint8_t> decoded_data(info.image_width * info.image_height * 4); |
| |
| if (info.buffered_image) { // Progressive JPEG. |
| if (!DecodeProgressiveJPEG(&info, decode_target_format, |
| decoded_data.data())) { |
| return kSbDecodeTargetInvalid; |
| } |
| } else if (!ReadLines(&info, decode_target_format, |
| decoded_data.data())) { // Baseline sequential JPEG. |
| return kSbDecodeTargetInvalid; |
| } |
| |
| if (!jpeg_finish_decompress(&info)) { |
| // In this case, we did read all the rows, so we don't really have to |
| // treat this as an error. |
| SB_DLOG(WARNING) << "Data source requests suspension of the decompressor."; |
| } |
| |
| auto width = info.image_width; |
| auto height = info.image_height; |
| jpeg_destroy_decompress(&info); |
| |
| return DecodeTargetCreate(context_provider, decoded_data.data(), width, |
| height, decode_target_format); |
| } |
| |
| } // namespace libjpeg |
| } // namespace shared |
| } // namespace starboard |