diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /image/decoders/nsJPEGDecoder.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'image/decoders/nsJPEGDecoder.cpp')
-rw-r--r-- | image/decoders/nsJPEGDecoder.cpp | 979 |
1 files changed, 979 insertions, 0 deletions
diff --git a/image/decoders/nsJPEGDecoder.cpp b/image/decoders/nsJPEGDecoder.cpp new file mode 100644 index 0000000000..61fb1126b6 --- /dev/null +++ b/image/decoders/nsJPEGDecoder.cpp @@ -0,0 +1,979 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ImageLogging.h" // Must appear first. + +#include "nsJPEGDecoder.h" + +#include <cstdint> + +#include "imgFrame.h" +#include "Orientation.h" +#include "EXIF.h" +#include "SurfacePipeFactory.h" + +#include "nspr.h" +#include "nsCRT.h" +#include "gfxColor.h" + +#include "jerror.h" + +#include "gfxPlatform.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/gfx/Types.h" +#include "mozilla/Telemetry.h" + +extern "C" { +#include "iccjpeg.h" +} + +#if MOZ_BIG_ENDIAN() +# define MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB JCS_EXT_XRGB +#else +# define MOZ_JCS_EXT_NATIVE_ENDIAN_XRGB JCS_EXT_BGRX +#endif + +static void cmyk_convert_bgra(uint32_t* aInput, uint32_t* aOutput, + int32_t aWidth); + +using mozilla::gfx::SurfaceFormat; + +namespace mozilla { +namespace image { + +static mozilla::LazyLogModule sJPEGLog("JPEGDecoder"); + +static mozilla::LazyLogModule sJPEGDecoderAccountingLog( + "JPEGDecoderAccounting"); + +static qcms_profile* GetICCProfile(struct jpeg_decompress_struct& info) { + JOCTET* profilebuf; + uint32_t profileLength; + qcms_profile* profile = nullptr; + + if (read_icc_profile(&info, &profilebuf, &profileLength)) { + profile = qcms_profile_from_memory(profilebuf, profileLength); + free(profilebuf); + } + + return profile; +} + +METHODDEF(void) init_source(j_decompress_ptr jd); +METHODDEF(boolean) fill_input_buffer(j_decompress_ptr jd); +METHODDEF(void) skip_input_data(j_decompress_ptr jd, long num_bytes); +METHODDEF(void) term_source(j_decompress_ptr jd); +METHODDEF(void) my_error_exit(j_common_ptr cinfo); + +// Normal JFIF markers can't have more bytes than this. +#define MAX_JPEG_MARKER_LENGTH (((uint32_t)1 << 16) - 1) + +nsJPEGDecoder::nsJPEGDecoder(RasterImage* aImage, + Decoder::DecodeStyle aDecodeStyle) + : Decoder(aImage), + mLexer(Transition::ToUnbuffered(State::FINISHED_JPEG_DATA, + State::JPEG_DATA, SIZE_MAX), + Transition::TerminateSuccess()), + mProfile(nullptr), + mProfileLength(0), + mCMSLine(nullptr), + mDecodeStyle(aDecodeStyle) { + this->mErr.pub.error_exit = nullptr; + this->mErr.pub.emit_message = nullptr; + this->mErr.pub.output_message = nullptr; + this->mErr.pub.format_message = nullptr; + this->mErr.pub.reset_error_mgr = nullptr; + this->mErr.pub.msg_code = 0; + this->mErr.pub.trace_level = 0; + this->mErr.pub.num_warnings = 0; + this->mErr.pub.jpeg_message_table = nullptr; + this->mErr.pub.last_jpeg_message = 0; + this->mErr.pub.addon_message_table = nullptr; + this->mErr.pub.first_addon_message = 0; + this->mErr.pub.last_addon_message = 0; + mState = JPEG_HEADER; + mReading = true; + mImageData = nullptr; + + mBytesToSkip = 0; + memset(&mInfo, 0, sizeof(jpeg_decompress_struct)); + memset(&mSourceMgr, 0, sizeof(mSourceMgr)); + mInfo.client_data = (void*)this; + + mSegment = nullptr; + mSegmentLen = 0; + + mBackBuffer = nullptr; + mBackBufferLen = mBackBufferSize = mBackBufferUnreadLen = 0; + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("nsJPEGDecoder::nsJPEGDecoder: Creating JPEG decoder %p", this)); +} + +nsJPEGDecoder::~nsJPEGDecoder() { + // Step 8: Release JPEG decompression object + mInfo.src = nullptr; + jpeg_destroy_decompress(&mInfo); + + free(mBackBuffer); + mBackBuffer = nullptr; + + delete[] mCMSLine; + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("nsJPEGDecoder::~nsJPEGDecoder: Destroying JPEG decoder %p", this)); +} + +Maybe<Telemetry::HistogramID> nsJPEGDecoder::SpeedHistogram() const { + return Some(Telemetry::IMAGE_DECODE_SPEED_JPEG); +} + +nsresult nsJPEGDecoder::InitInternal() { + // We set up the normal JPEG error routines, then override error_exit. + mInfo.err = jpeg_std_error(&mErr.pub); + // mInfo.err = jpeg_std_error(&mErr.pub); + mErr.pub.error_exit = my_error_exit; + // Establish the setjmp return context for my_error_exit to use. + if (setjmp(mErr.setjmp_buffer)) { + // If we get here, the JPEG code has signaled an error, and initialization + // has failed. + return NS_ERROR_FAILURE; + } + + // Step 1: allocate and initialize JPEG decompression object + jpeg_create_decompress(&mInfo); + // Set the source manager + mInfo.src = &mSourceMgr; + + // Step 2: specify data source (eg, a file) + + // Setup callback functions. + mSourceMgr.init_source = init_source; + mSourceMgr.fill_input_buffer = fill_input_buffer; + mSourceMgr.skip_input_data = skip_input_data; + mSourceMgr.resync_to_restart = jpeg_resync_to_restart; + mSourceMgr.term_source = term_source; + + // Record app markers for ICC data + for (uint32_t m = 0; m < 16; m++) { + jpeg_save_markers(&mInfo, JPEG_APP0 + m, 0xFFFF); + } + + return NS_OK; +} + +nsresult nsJPEGDecoder::FinishInternal() { + // If we're not in any sort of error case, force our state to JPEG_DONE. + if ((mState != JPEG_DONE && mState != JPEG_SINK_NON_JPEG_TRAILER) && + (mState != JPEG_ERROR) && !IsMetadataDecode()) { + mState = JPEG_DONE; + } + + return NS_OK; +} + +LexerResult nsJPEGDecoder::DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) { + MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); + + return mLexer.Lex(aIterator, aOnResume, + [=](State aState, const char* aData, size_t aLength) { + switch (aState) { + case State::JPEG_DATA: + return ReadJPEGData(aData, aLength); + case State::FINISHED_JPEG_DATA: + return FinishedJPEGData(); + } + MOZ_CRASH("Unknown State"); + }); +} + +LexerTransition<nsJPEGDecoder::State> nsJPEGDecoder::ReadJPEGData( + const char* aData, size_t aLength) { + mSegment = reinterpret_cast<const JOCTET*>(aData); + mSegmentLen = aLength; + + // Return here if there is a error within libjpeg. + nsresult error_code; + // This cast to nsresult makes sense because setjmp() returns whatever we + // passed to longjmp(), which was actually an nsresult. These error codes + // have been translated from libjpeg error codes, like so: + // JERR_OUT_OF_MEMORY => NS_ERROR_OUT_OF_MEMORY + // JERR_UNKNOWN_MARKER => NS_ERROR_ILLEGAL_VALUE + // JERR_SOF_UNSUPPORTED => NS_ERROR_INVALID_CONTENT_ENCODING + // <any other error> => NS_ERROR_FAILURE + if ((error_code = static_cast<nsresult>(setjmp(mErr.setjmp_buffer))) != + NS_OK) { + bool fatal = true; + if (error_code == NS_ERROR_FAILURE) { + // Error due to corrupt data. Make sure that we don't feed any more data + // to libjpeg-turbo. + mState = JPEG_SINK_NON_JPEG_TRAILER; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned NS_ERROR_FAILURE)")); + } else if (error_code == NS_ERROR_ILLEGAL_VALUE) { + // This is a recoverable error. Consume the marker and continue. + mInfo.unread_marker = 0; + fatal = false; + } else if (error_code == NS_ERROR_INVALID_CONTENT_ENCODING) { + // The content is encoding frames with a format that libjpeg can't handle. + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned NS_ERROR_INVALID_CONTENT_ENCODING)")); + // Check to see if we're in the done state, which indicates that we've + // already processed the main JPEG data. + bool inDoneState = (mState == JPEG_DONE); + // Whether we succeed or fail, we shouldn't send any more data. + mState = JPEG_SINK_NON_JPEG_TRAILER; + + // If we're in the done state, we exit successfully and attempt to + // display the content we've already received. Otherwise, we fallthrough + // and treat this as a fatal error. + if (inDoneState) { + return Transition::TerminateSuccess(); + } + } else { + // Error for another reason. (Possibly OOM.) + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (setjmp returned an error)")); + } + + if (fatal) { + return Transition::TerminateFailure(); + } + } + + MOZ_LOG(sJPEGLog, LogLevel::Debug, + ("[this=%p] nsJPEGDecoder::Write -- processing JPEG data\n", this)); + + switch (mState) { + case JPEG_HEADER: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- entering JPEG_HEADER" + " case"); + + // Step 3: read file parameters with jpeg_read_header() + if (jpeg_read_header(&mInfo, TRUE) == JPEG_SUSPENDED) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (JPEG_SUSPENDED)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + // Post our size to the superclass + EXIFData exif = ReadExifData(); + PostSize(mInfo.image_width, mInfo.image_height, exif.orientation, + exif.resolution); + if (HasError()) { + // Setting the size led to an error. + mState = JPEG_ERROR; + return Transition::TerminateFailure(); + } + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + return Transition::TerminateSuccess(); + } + + // We're doing a full decode. + switch (mInfo.jpeg_color_space) { + case JCS_GRAYSCALE: + case JCS_RGB: + case JCS_YCbCr: + // By default, we will output directly to BGRA. If we need to apply + // special color transforms, this may change. + switch (SurfaceFormat::OS_RGBX) { + case SurfaceFormat::B8G8R8X8: + mInfo.out_color_space = JCS_EXT_BGRX; + break; + case SurfaceFormat::X8R8G8B8: + mInfo.out_color_space = JCS_EXT_XRGB; + break; + case SurfaceFormat::R8G8B8X8: + mInfo.out_color_space = JCS_EXT_RGBX; + break; + default: + mState = JPEG_ERROR; + return Transition::TerminateFailure(); + } + break; + case JCS_CMYK: + case JCS_YCCK: + // libjpeg can convert from YCCK to CMYK, but not to XRGB. + mInfo.out_color_space = JCS_CMYK; + break; + default: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (unknown colorspace (3))")); + return Transition::TerminateFailure(); + } + + if (mCMSMode != CMSMode::Off) { + if ((mInProfile = GetICCProfile(mInfo)) != nullptr && + GetCMSOutputProfile()) { + uint32_t profileSpace = qcms_profile_get_color_space(mInProfile); + + qcms_data_type outputType = gfxPlatform::GetCMSOSRGBAType(); + Maybe<qcms_data_type> inputType; + if (profileSpace == icSigRgbData) { + // We can always color manage RGB profiles since it happens at the + // end of the pipeline. + inputType.emplace(outputType); + } else if (profileSpace == icSigGrayData && + mInfo.jpeg_color_space == JCS_GRAYSCALE) { + // We can only color manage gray profiles if the original color + // space is grayscale. This means we must downscale after color + // management since the downscaler assumes BGRA. + mInfo.out_color_space = JCS_GRAYSCALE; + inputType.emplace(QCMS_DATA_GRAY_8); + } + +#if 0 + // We don't currently support CMYK profiles. The following + // code dealt with lcms types. Add something like this + // back when we gain support for CMYK. + + // Adobe Photoshop writes YCCK/CMYK files with inverted data + if (mInfo.out_color_space == JCS_CMYK) { + type |= FLAVOR_SH(mInfo.saw_Adobe_marker ? 1 : 0); + } +#endif + + if (inputType) { + // Calculate rendering intent. + int intent = gfxPlatform::GetRenderingIntent(); + if (intent == -1) { + intent = qcms_profile_get_rendering_intent(mInProfile); + } + + // Create the color management transform. + mTransform = qcms_transform_create(mInProfile, *inputType, + GetCMSOutputProfile(), + outputType, (qcms_intent)intent); + } + } else if (mCMSMode == CMSMode::All) { + mTransform = GetCMSsRGBTransform(SurfaceFormat::OS_RGBX); + } + } + + // We don't want to use the pipe buffers directly because we don't want + // any reads on non-BGRA formatted data. + if (mInfo.out_color_space == JCS_GRAYSCALE || + mInfo.out_color_space == JCS_CMYK) { + mCMSLine = new (std::nothrow) uint32_t[mInfo.image_width]; + if (!mCMSLine) { + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (could allocate buffer for color conversion)")); + return Transition::TerminateFailure(); + } + } + + // Don't allocate a giant and superfluous memory buffer + // when not doing a progressive decode. + mInfo.buffered_image = + mDecodeStyle == PROGRESSIVE && jpeg_has_multiple_scans(&mInfo); + + /* Used to set up image size so arrays can be allocated */ + jpeg_calc_output_dimensions(&mInfo); + + // We handle the transform outside the pipeline if we are outputting in + // grayscale, because the pipeline wants BGRA pixels, particularly the + // downscaling filter, so we can't handle it after downscaling as would + // be optimal. + qcms_transform* pipeTransform = + mInfo.out_color_space != JCS_GRAYSCALE ? mTransform : nullptr; + + Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateReorientSurfacePipe( + this, Size(), OutputSize(), SurfaceFormat::OS_RGBX, pipeTransform, + GetOrientation()); + if (!pipe) { + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (could not initialize surface pipe)")); + return Transition::TerminateFailure(); + } + + mPipe = std::move(*pipe); + + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + (" JPEGDecoderAccounting: nsJPEGDecoder::" + "Write -- created image frame with %ux%u pixels", + mInfo.image_width, mInfo.image_height)); + + mState = JPEG_START_DECOMPRESS; + [[fallthrough]]; // to start decompressing. + } + + case JPEG_START_DECOMPRESS: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- entering" + " JPEG_START_DECOMPRESS case"); + // Step 4: set parameters for decompression + + // FIXME -- Should reset dct_method and dither mode + // for final pass of progressive JPEG + + mInfo.dct_method = JDCT_ISLOW; + mInfo.dither_mode = JDITHER_FS; + mInfo.do_fancy_upsampling = TRUE; + mInfo.enable_2pass_quant = FALSE; + mInfo.do_block_smoothing = TRUE; + + // Step 5: Start decompressor + if (jpeg_start_decompress(&mInfo) == FALSE) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_start_decompress())")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + // If this is a progressive JPEG ... + mState = mInfo.buffered_image ? JPEG_DECOMPRESS_PROGRESSIVE + : JPEG_DECOMPRESS_SEQUENTIAL; + [[fallthrough]]; // to decompress sequential JPEG. + } + + case JPEG_DECOMPRESS_SEQUENTIAL: { + if (mState == JPEG_DECOMPRESS_SEQUENTIAL) { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- " + "JPEG_DECOMPRESS_SEQUENTIAL case"); + + switch (OutputScanlines()) { + case WriteState::NEED_MORE_DATA: + MOZ_LOG( + sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after OutputScanlines() - SEQUENTIAL)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + case WriteState::FINISHED: + NS_ASSERTION(mInfo.output_scanline == mInfo.output_height, + "We didn't process all of the data!"); + mState = JPEG_DONE; + break; + case WriteState::FAILURE: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (Error in pipeline from OutputScalines())")); + return Transition::TerminateFailure(); + } + } + [[fallthrough]]; // to decompress progressive JPEG. + } + + case JPEG_DECOMPRESS_PROGRESSIVE: { + if (mState == JPEG_DECOMPRESS_PROGRESSIVE) { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::Write -- JPEG_DECOMPRESS_PROGRESSIVE case"); + auto AllComponentsSeen = [](jpeg_decompress_struct& mInfo) { + bool all_components_seen = true; + if (mInfo.coef_bits) { + for (int c = 0; c < mInfo.num_components; ++c) { + bool current_component_seen = mInfo.coef_bits[c][0] != -1; + all_components_seen &= current_component_seen; + } + } + return all_components_seen; + }; + int status; + int scan_to_display_first = 0; + bool all_components_seen; + all_components_seen = AllComponentsSeen(mInfo); + if (all_components_seen) { + scan_to_display_first = mInfo.input_scan_number; + } + + do { + status = jpeg_consume_input(&mInfo); + + if (status == JPEG_REACHED_SOS || status == JPEG_REACHED_EOI || + status == JPEG_SUSPENDED) { + // record the first scan where all components are present + all_components_seen = AllComponentsSeen(mInfo); + if (!scan_to_display_first && all_components_seen) { + scan_to_display_first = mInfo.input_scan_number; + } + } + } while ((status != JPEG_SUSPENDED) && (status != JPEG_REACHED_EOI)); + + if (!all_components_seen) { + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + // make sure we never try to access the non-exsitent scan 0 + if (!scan_to_display_first) { + scan_to_display_first = 1; + } + while (mState != JPEG_DONE) { + if (mInfo.output_scanline == 0) { + int scan = mInfo.input_scan_number; + + // if we haven't displayed anything yet (output_scan_number==0) + // and we have enough data for a complete scan, force output + // of the last full scan, but only if this last scan has seen + // DC data from all components + if ((mInfo.output_scan_number == 0) && + (scan > scan_to_display_first) && + (status != JPEG_REACHED_EOI)) { + scan--; + } + MOZ_ASSERT(scan > 0, "scan number to small!"); + if (!jpeg_start_output(&mInfo, scan)) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_start_output() -" + " PROGRESSIVE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + } + + if (mInfo.output_scanline == 0xffffff) { + mInfo.output_scanline = 0; + } + + switch (OutputScanlines()) { + case WriteState::NEED_MORE_DATA: + if (mInfo.output_scanline == 0) { + // didn't manage to read any lines - flag so we don't call + // jpeg_start_output() multiple times for the same scan + mInfo.output_scanline = 0xffffff; + } + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after OutputScanlines() - " + "PROGRESSIVE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + case WriteState::FINISHED: + NS_ASSERTION(mInfo.output_scanline == mInfo.output_height, + "We didn't process all of the data!"); + + if (!jpeg_finish_output(&mInfo)) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_finish_output() -" + " PROGRESSIVE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + if (jpeg_input_complete(&mInfo) && + (mInfo.input_scan_number == mInfo.output_scan_number)) { + mState = JPEG_DONE; + } else { + mInfo.output_scanline = 0; + mPipe.ResetToFirstRow(); + } + break; + case WriteState::FAILURE: + mState = JPEG_ERROR; + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (Error in pipeline from OutputScalines())")); + return Transition::TerminateFailure(); + } + } + } + [[fallthrough]]; // to finish decompressing. + } + + case JPEG_DONE: { + LOG_SCOPE((mozilla::LogModule*)sJPEGLog, + "nsJPEGDecoder::ProcessData -- entering" + " JPEG_DONE case"); + + // Step 7: Finish decompression + + if (jpeg_finish_decompress(&mInfo) == FALSE) { + MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, + ("} (I/O suspension after jpeg_finish_decompress() - DONE)")); + return Transition::ContinueUnbuffered( + State::JPEG_DATA); // I/O suspension + } + + // Make sure we don't feed any more data to libjpeg-turbo. + mState = JPEG_SINK_NON_JPEG_TRAILER; + + // We're done. + return Transition::TerminateSuccess(); + } + case JPEG_SINK_NON_JPEG_TRAILER: + MOZ_LOG(sJPEGLog, LogLevel::Debug, + ("[this=%p] nsJPEGDecoder::ProcessData -- entering" + " JPEG_SINK_NON_JPEG_TRAILER case\n", + this)); + + MOZ_ASSERT_UNREACHABLE( + "Should stop getting data after entering state " + "JPEG_SINK_NON_JPEG_TRAILER"); + + return Transition::TerminateSuccess(); + + case JPEG_ERROR: + MOZ_ASSERT_UNREACHABLE( + "Should stop getting data after entering state " + "JPEG_ERROR"); + + return Transition::TerminateFailure(); + } + + MOZ_ASSERT_UNREACHABLE("Escaped the JPEG decoder state machine"); + return Transition::TerminateFailure(); +} // namespace image + +LexerTransition<nsJPEGDecoder::State> nsJPEGDecoder::FinishedJPEGData() { + // Since we set up an unbuffered read for SIZE_MAX bytes, if we actually read + // all that data something is really wrong. + MOZ_ASSERT_UNREACHABLE("Read the entire address space?"); + return Transition::TerminateFailure(); +} + +EXIFData nsJPEGDecoder::ReadExifData() const { + jpeg_saved_marker_ptr marker; + + // Locate the APP1 marker, where EXIF data is stored, in the marker list. + for (marker = mInfo.marker_list; marker != nullptr; marker = marker->next) { + if (marker->marker == JPEG_APP0 + 1) { + break; + } + } + + // If we're at the end of the list, there's no EXIF data. + if (!marker) { + return EXIFData(); + } + + return EXIFParser::Parse(marker->data, + static_cast<uint32_t>(marker->data_length), + gfx::IntSize(mInfo.image_width, mInfo.image_height)); +} + +void nsJPEGDecoder::NotifyDone() { + PostFrameStop(Opacity::FULLY_OPAQUE); + PostDecodeDone(); +} + +WriteState nsJPEGDecoder::OutputScanlines() { + auto result = mPipe.WritePixelBlocks<uint32_t>( + [&](uint32_t* aPixelBlock, int32_t aBlockSize) { + JSAMPROW sampleRow = (JSAMPROW)(mCMSLine ? mCMSLine : aPixelBlock); + if (jpeg_read_scanlines(&mInfo, &sampleRow, 1) != 1) { + return MakeTuple(/* aWritten */ 0, Some(WriteState::NEED_MORE_DATA)); + } + + switch (mInfo.out_color_space) { + default: + // Already outputted directly to aPixelBlock as BGRA. + MOZ_ASSERT(!mCMSLine); + break; + case JCS_GRAYSCALE: + // The transform here does both color management, and converts the + // pixels from grayscale to BGRA. This is why we do it here, instead + // of using ColorManagementFilter in the SurfacePipe, because the + // other filters (e.g. DownscalingFilter) require BGRA pixels. + MOZ_ASSERT(mCMSLine); + qcms_transform_data(mTransform, mCMSLine, aPixelBlock, + mInfo.output_width); + break; + case JCS_CMYK: + // Convert from CMYK to BGRA + MOZ_ASSERT(mCMSLine); + cmyk_convert_bgra(mCMSLine, aPixelBlock, aBlockSize); + break; + } + + return MakeTuple(aBlockSize, Maybe<WriteState>()); + }); + + Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect(); + if (invalidRect) { + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); + } + + return result; +} + +// Override the standard error method in the IJG JPEG decoder code. +METHODDEF(void) +my_error_exit(j_common_ptr cinfo) { + decoder_error_mgr* err = (decoder_error_mgr*)cinfo->err; + + // Convert error to a browser error code + nsresult error_code; + switch (err->pub.msg_code) { + case JERR_OUT_OF_MEMORY: + error_code = NS_ERROR_OUT_OF_MEMORY; + break; + case JERR_UNKNOWN_MARKER: + error_code = NS_ERROR_ILLEGAL_VALUE; + break; + case JERR_SOF_UNSUPPORTED: + error_code = NS_ERROR_INVALID_CONTENT_ENCODING; + break; + default: + error_code = NS_ERROR_FAILURE; + } + +#ifdef DEBUG + char buffer[JMSG_LENGTH_MAX]; + + // Create the message + (*err->pub.format_message)(cinfo, buffer); + + fprintf(stderr, "JPEG decoding error:\n%s\n", buffer); +#endif + + // Return control to the setjmp point. We pass an nsresult masquerading as + // an int, which works because the setjmp() caller casts it back. + longjmp(err->setjmp_buffer, static_cast<int>(error_code)); +} + +/******************************************************************************* + * This is the callback routine from the IJG JPEG library used to supply new + * data to the decompressor when its input buffer is exhausted. It juggles + * multiple buffers in an attempt to avoid unnecessary copying of input data. + * + * (A simpler scheme is possible: It's much easier to use only a single + * buffer; when fill_input_buffer() is called, move any unconsumed data + * (beyond the current pointer/count) down to the beginning of this buffer and + * then load new data into the remaining buffer space. This approach requires + * a little more data copying but is far easier to get right.) + * + * At any one time, the JPEG decompressor is either reading from the necko + * input buffer, which is volatile across top-level calls to the IJG library, + * or the "backtrack" buffer. The backtrack buffer contains the remaining + * unconsumed data from the necko buffer after parsing was suspended due + * to insufficient data in some previous call to the IJG library. + * + * When suspending, the decompressor will back up to a convenient restart + * point (typically the start of the current MCU). The variables + * next_input_byte & bytes_in_buffer indicate where the restart point will be + * if the current call returns FALSE. Data beyond this point must be + * rescanned after resumption, so it must be preserved in case the decompressor + * decides to backtrack. + * + * Returns: + * TRUE if additional data is available, FALSE if no data present and + * the JPEG library should therefore suspend processing of input stream + ******************************************************************************/ + +/******************************************************************************/ +/* data source manager method */ +/******************************************************************************/ + +/******************************************************************************/ +/* data source manager method + Initialize source. This is called by jpeg_read_header() before any + data is actually read. May leave + bytes_in_buffer set to 0 (in which case a fill_input_buffer() call + will occur immediately). +*/ +METHODDEF(void) +init_source(j_decompress_ptr jd) {} + +/******************************************************************************/ +/* data source manager method + Skip num_bytes worth of data. The buffer pointer and count should + be advanced over num_bytes input bytes, refilling the buffer as + needed. This is used to skip over a potentially large amount of + uninteresting data (such as an APPn marker). In some applications + it may be possible to optimize away the reading of the skipped data, + but it's not clear that being smart is worth much trouble; large + skips are uncommon. bytes_in_buffer may be zero on return. + A zero or negative skip count should be treated as a no-op. +*/ +METHODDEF(void) +skip_input_data(j_decompress_ptr jd, long num_bytes) { + struct jpeg_source_mgr* src = jd->src; + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + if (num_bytes > (long)src->bytes_in_buffer) { + // Can't skip it all right now until we get more data from + // network stream. Set things up so that fill_input_buffer + // will skip remaining amount. + decoder->mBytesToSkip = (size_t)num_bytes - src->bytes_in_buffer; + src->next_input_byte += src->bytes_in_buffer; + src->bytes_in_buffer = 0; + + } else { + // Simple case. Just advance buffer pointer + + src->bytes_in_buffer -= (size_t)num_bytes; + src->next_input_byte += num_bytes; + } +} + +/******************************************************************************/ +/* data source manager method + This is called whenever bytes_in_buffer has reached zero and more + data is wanted. In typical applications, it should read fresh data + into the buffer (ignoring the current state of next_input_byte and + bytes_in_buffer), reset the pointer & count to the start of the + buffer, and return TRUE indicating that the buffer has been reloaded. + It is not necessary to fill the buffer entirely, only to obtain at + least one more byte. bytes_in_buffer MUST be set to a positive value + if TRUE is returned. A FALSE return should only be used when I/O + suspension is desired. +*/ +METHODDEF(boolean) +fill_input_buffer(j_decompress_ptr jd) { + struct jpeg_source_mgr* src = jd->src; + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + if (decoder->mReading) { + const JOCTET* new_buffer = decoder->mSegment; + uint32_t new_buflen = decoder->mSegmentLen; + + if (!new_buffer || new_buflen == 0) { + return false; // suspend + } + + decoder->mSegmentLen = 0; + + if (decoder->mBytesToSkip) { + if (decoder->mBytesToSkip < new_buflen) { + // All done skipping bytes; Return what's left. + new_buffer += decoder->mBytesToSkip; + new_buflen -= decoder->mBytesToSkip; + decoder->mBytesToSkip = 0; + } else { + // Still need to skip some more data in the future + decoder->mBytesToSkip -= (size_t)new_buflen; + return false; // suspend + } + } + + decoder->mBackBufferUnreadLen = src->bytes_in_buffer; + + src->next_input_byte = new_buffer; + src->bytes_in_buffer = (size_t)new_buflen; + decoder->mReading = false; + + return true; + } + + if (src->next_input_byte != decoder->mSegment) { + // Backtrack data has been permanently consumed. + decoder->mBackBufferUnreadLen = 0; + decoder->mBackBufferLen = 0; + } + + // Save remainder of netlib buffer in backtrack buffer + const uint32_t new_backtrack_buflen = + src->bytes_in_buffer + decoder->mBackBufferLen; + + // Make sure backtrack buffer is big enough to hold new data. + if (decoder->mBackBufferSize < new_backtrack_buflen) { + // Check for malformed MARKER segment lengths, before allocating space + // for it + if (new_backtrack_buflen > MAX_JPEG_MARKER_LENGTH) { + my_error_exit((j_common_ptr)(&decoder->mInfo)); + } + + // Round up to multiple of 256 bytes. + const size_t roundup_buflen = ((new_backtrack_buflen + 255) >> 8) << 8; + JOCTET* buf = (JOCTET*)realloc(decoder->mBackBuffer, roundup_buflen); + // Check for OOM + if (!buf) { + decoder->mInfo.err->msg_code = JERR_OUT_OF_MEMORY; + my_error_exit((j_common_ptr)(&decoder->mInfo)); + } + decoder->mBackBuffer = buf; + decoder->mBackBufferSize = roundup_buflen; + } + + // Ensure we actually have a backtrack buffer. Without it, then we know that + // there is no data to copy and bytes_in_buffer is already zero. + if (decoder->mBackBuffer) { + // Copy remainder of netlib segment into backtrack buffer. + memmove(decoder->mBackBuffer + decoder->mBackBufferLen, + src->next_input_byte, src->bytes_in_buffer); + } else { + MOZ_ASSERT(src->bytes_in_buffer == 0); + MOZ_ASSERT(decoder->mBackBufferLen == 0); + MOZ_ASSERT(decoder->mBackBufferUnreadLen == 0); + } + + // Point to start of data to be rescanned. + src->next_input_byte = decoder->mBackBuffer + decoder->mBackBufferLen - + decoder->mBackBufferUnreadLen; + src->bytes_in_buffer += decoder->mBackBufferUnreadLen; + decoder->mBackBufferLen = (size_t)new_backtrack_buflen; + decoder->mReading = true; + + return false; +} + +/******************************************************************************/ +/* data source manager method */ +/* + * Terminate source --- called by jpeg_finish_decompress() after all + * data has been read to clean up JPEG source manager. NOT called by + * jpeg_abort() or jpeg_destroy(). + */ +METHODDEF(void) +term_source(j_decompress_ptr jd) { + nsJPEGDecoder* decoder = (nsJPEGDecoder*)(jd->client_data); + + // This function shouldn't be called if we ran into an error we didn't + // recover from. + MOZ_ASSERT(decoder->mState != JPEG_ERROR, + "Calling term_source on a JPEG with mState == JPEG_ERROR!"); + + // Notify using a helper method to get around protectedness issues. + decoder->NotifyDone(); +} + +} // namespace image +} // namespace mozilla + +///*************** Inverted CMYK -> RGB conversion ************************* +/// Input is (Inverted) CMYK stored as 4 bytes per pixel. +/// Output is RGB stored as 3 bytes per pixel. +/// @param aInput Points to row buffer containing the CMYK bytes for each pixel +/// in the row. +/// @param aOutput Points to row buffer to write BGRA to. +/// @param aWidth Number of pixels in the row. +static void cmyk_convert_bgra(uint32_t* aInput, uint32_t* aOutput, + int32_t aWidth) { + uint8_t* input = reinterpret_cast<uint8_t*>(aInput); + + for (int32_t i = 0; i < aWidth; ++i) { + // Source is 'Inverted CMYK', output is RGB. + // See: http://www.easyrgb.com/math.php?MATH=M12#text12 + // Or: http://www.ilkeratalay.com/colorspacesfaq.php#rgb + + // From CMYK to CMY + // C = ( C * ( 1 - K ) + K ) + // M = ( M * ( 1 - K ) + K ) + // Y = ( Y * ( 1 - K ) + K ) + + // From Inverted CMYK to CMY is thus: + // C = ( (1-iC) * (1 - (1-iK)) + (1-iK) ) => 1 - iC*iK + // Same for M and Y + + // Convert from CMY (0..1) to RGB (0..1) + // R = 1 - C => 1 - (1 - iC*iK) => iC*iK + // G = 1 - M => 1 - (1 - iM*iK) => iM*iK + // B = 1 - Y => 1 - (1 - iY*iK) => iY*iK + + // Convert from Inverted CMYK (0..255) to RGB (0..255) + const uint32_t iC = input[0]; + const uint32_t iM = input[1]; + const uint32_t iY = input[2]; + const uint32_t iK = input[3]; + + const uint8_t r = iC * iK / 255; + const uint8_t g = iM * iK / 255; + const uint8_t b = iY * iK / 255; + + *aOutput++ = (0xFF << mozilla::gfx::SurfaceFormatBit::OS_A) | + (r << mozilla::gfx::SurfaceFormatBit::OS_R) | + (g << mozilla::gfx::SurfaceFormatBit::OS_G) | + (b << mozilla::gfx::SurfaceFormatBit::OS_B); + input += 4; + } +} |