/* -*- 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 "nsPNGDecoder.h" #include #include #include "gfxColor.h" #include "gfxPlatform.h" #include "imgFrame.h" #include "nsColor.h" #include "nsIInputStream.h" #include "nsMemory.h" #include "nsRect.h" #include "nspr.h" #include "png.h" #include "RasterImage.h" #include "SurfacePipeFactory.h" #include "mozilla/DebugOnly.h" #include "mozilla/Telemetry.h" using namespace mozilla::gfx; using std::min; namespace mozilla { namespace image { static LazyLogModule sPNGLog("PNGDecoder"); static LazyLogModule sPNGDecoderAccountingLog("PNGDecoderAccounting"); // limit image dimensions (bug #251381, #591822, #967656, and #1283961) #ifndef MOZ_PNG_MAX_WIDTH # define MOZ_PNG_MAX_WIDTH 0x7fffffff // Unlimited #endif #ifndef MOZ_PNG_MAX_HEIGHT # define MOZ_PNG_MAX_HEIGHT 0x7fffffff // Unlimited #endif nsPNGDecoder::AnimFrameInfo::AnimFrameInfo() : mDispose(DisposalMethod::KEEP) , mBlend(BlendMethod::OVER) , mTimeout(0) { } #ifdef PNG_APNG_SUPPORTED int32_t GetNextFrameDelay(png_structp aPNG, png_infop aInfo) { // Delay, in seconds, is delayNum / delayDen. png_uint_16 delayNum = png_get_next_frame_delay_num(aPNG, aInfo); png_uint_16 delayDen = png_get_next_frame_delay_den(aPNG, aInfo); if (delayNum == 0) { return 0; // SetFrameTimeout() will set to a minimum. } if (delayDen == 0) { delayDen = 100; // So says the APNG spec. } // Need to cast delay_num to float to have a proper division and // the result to int to avoid a compiler warning. return static_cast(static_cast(delayNum) * 1000 / delayDen); } nsPNGDecoder::AnimFrameInfo::AnimFrameInfo(png_structp aPNG, png_infop aInfo) : mDispose(DisposalMethod::KEEP) , mBlend(BlendMethod::OVER) , mTimeout(0) { png_byte dispose_op = png_get_next_frame_dispose_op(aPNG, aInfo); png_byte blend_op = png_get_next_frame_blend_op(aPNG, aInfo); if (dispose_op == PNG_DISPOSE_OP_PREVIOUS) { mDispose = DisposalMethod::RESTORE_PREVIOUS; } else if (dispose_op == PNG_DISPOSE_OP_BACKGROUND) { mDispose = DisposalMethod::CLEAR; } else { mDispose = DisposalMethod::KEEP; } if (blend_op == PNG_BLEND_OP_SOURCE) { mBlend = BlendMethod::SOURCE; } else { mBlend = BlendMethod::OVER; } mTimeout = GetNextFrameDelay(aPNG, aInfo); } #endif // First 8 bytes of a PNG file const uint8_t nsPNGDecoder::pngSignatureBytes[] = { 137, 80, 78, 71, 13, 10, 26, 10 }; nsPNGDecoder::nsPNGDecoder(RasterImage* aImage) : Decoder(aImage) , mLexer(Transition::ToUnbuffered(State::FINISHED_PNG_DATA, State::PNG_DATA, SIZE_MAX), Transition::TerminateSuccess()) , mNextTransition(Transition::ContinueUnbuffered(State::PNG_DATA)) , mLastChunkLength(0) , mPNG(nullptr) , mInfo(nullptr) , mCMSLine(nullptr) , interlacebuf(nullptr) , mInProfile(nullptr) , mTransform(nullptr) , format(gfx::SurfaceFormat::UNKNOWN) , mCMSMode(0) , mChannels(0) , mPass(0) , mFrameIsHidden(false) , mDisablePremultipliedAlpha(false) , mNumFrames(0) { } nsPNGDecoder::~nsPNGDecoder() { if (mPNG) { png_destroy_read_struct(&mPNG, mInfo ? &mInfo : nullptr, nullptr); } if (mCMSLine) { free(mCMSLine); } if (interlacebuf) { free(interlacebuf); } if (mInProfile) { qcms_profile_release(mInProfile); // mTransform belongs to us only if mInProfile is non-null if (mTransform) { qcms_transform_release(mTransform); } } } nsPNGDecoder::TransparencyType nsPNGDecoder::GetTransparencyType(SurfaceFormat aFormat, const IntRect& aFrameRect) { // Check if the image has a transparent color in its palette. if (aFormat == SurfaceFormat::B8G8R8A8) { return TransparencyType::eAlpha; } if (!aFrameRect.IsEqualEdges(FullFrame())) { MOZ_ASSERT(HasAnimation()); return TransparencyType::eFrameRect; } return TransparencyType::eNone; } void nsPNGDecoder::PostHasTransparencyIfNeeded(TransparencyType aTransparencyType) { switch (aTransparencyType) { case TransparencyType::eNone: return; case TransparencyType::eAlpha: PostHasTransparency(); return; case TransparencyType::eFrameRect: // If the first frame of animated image doesn't draw into the whole image, // then record that it is transparent. For subsequent frames, this doesn't // affect transparency, because they're composited on top of all previous // frames. if (mNumFrames == 0) { PostHasTransparency(); } return; } } // CreateFrame() is used for both simple and animated images. nsresult nsPNGDecoder::CreateFrame(const FrameInfo& aFrameInfo) { MOZ_ASSERT(HasSize()); MOZ_ASSERT(!IsMetadataDecode()); // Check if we have transparency, and send notifications if needed. auto transparency = GetTransparencyType(aFrameInfo.mFormat, aFrameInfo.mFrameRect); PostHasTransparencyIfNeeded(transparency); SurfaceFormat format = transparency == TransparencyType::eNone ? SurfaceFormat::B8G8R8X8 : SurfaceFormat::B8G8R8A8; // Make sure there's no animation or padding if we're downscaling. MOZ_ASSERT_IF(Size() != OutputSize(), mNumFrames == 0); MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation()); MOZ_ASSERT_IF(Size() != OutputSize(), transparency != TransparencyType::eFrameRect); // If this image is interlaced, we can display better quality intermediate // results to the user by post processing them with ADAM7InterpolatingFilter. SurfacePipeFlags pipeFlags = aFrameInfo.mIsInterlaced ? SurfacePipeFlags::ADAM7_INTERPOLATE : SurfacePipeFlags(); if (mNumFrames == 0) { // The first frame may be displayed progressively. pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; } Maybe pipe = SurfacePipeFactory::CreateSurfacePipe(this, mNumFrames, Size(), OutputSize(), aFrameInfo.mFrameRect, format, pipeFlags); if (!pipe) { mPipe = SurfacePipe(); return NS_ERROR_FAILURE; } mPipe = Move(*pipe); mFrameRect = aFrameInfo.mFrameRect; mPass = 0; MOZ_LOG(sPNGDecoderAccountingLog, LogLevel::Debug, ("PNGDecoderAccounting: nsPNGDecoder::CreateFrame -- created " "image frame with %dx%d pixels for decoder %p", mFrameRect.width, mFrameRect.height, this)); #ifdef PNG_APNG_SUPPORTED if (png_get_valid(mPNG, mInfo, PNG_INFO_acTL)) { mAnimInfo = AnimFrameInfo(mPNG, mInfo); if (mAnimInfo.mDispose == DisposalMethod::CLEAR) { // We may have to display the background under this image during // animation playback, so we regard it as transparent. PostHasTransparency(); } } #endif return NS_OK; } // set timeout and frame disposal method for the current frame void nsPNGDecoder::EndImageFrame() { if (mFrameIsHidden) { return; } mNumFrames++; Opacity opacity = Opacity::SOME_TRANSPARENCY; if (format == gfx::SurfaceFormat::B8G8R8X8) { opacity = Opacity::FULLY_OPAQUE; } PostFrameStop(opacity, mAnimInfo.mDispose, FrameTimeout::FromRawMilliseconds(mAnimInfo.mTimeout), mAnimInfo.mBlend, Some(mFrameRect)); } nsresult nsPNGDecoder::InitInternal() { mCMSMode = gfxPlatform::GetCMSMode(); if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) { mCMSMode = eCMSMode_Off; } mDisablePremultipliedAlpha = bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA); #ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED static png_byte color_chunks[]= { 99, 72, 82, 77, '\0', // cHRM 105, 67, 67, 80, '\0'}; // iCCP static png_byte unused_chunks[]= { 98, 75, 71, 68, '\0', // bKGD 104, 73, 83, 84, '\0', // hIST 105, 84, 88, 116, '\0', // iTXt 111, 70, 70, 115, '\0', // oFFs 112, 67, 65, 76, '\0', // pCAL 115, 67, 65, 76, '\0', // sCAL 112, 72, 89, 115, '\0', // pHYs 115, 66, 73, 84, '\0', // sBIT 115, 80, 76, 84, '\0', // sPLT 116, 69, 88, 116, '\0', // tEXt 116, 73, 77, 69, '\0', // tIME 122, 84, 88, 116, '\0'}; // zTXt #endif // Initialize the container's source image header // Always decode to 24 bit pixdepth mPNG = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nsPNGDecoder::error_callback, nsPNGDecoder::warning_callback); if (!mPNG) { return NS_ERROR_OUT_OF_MEMORY; } mInfo = png_create_info_struct(mPNG); if (!mInfo) { png_destroy_read_struct(&mPNG, nullptr, nullptr); return NS_ERROR_OUT_OF_MEMORY; } #ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED // Ignore unused chunks if (mCMSMode == eCMSMode_Off || IsMetadataDecode()) { png_set_keep_unknown_chunks(mPNG, 1, color_chunks, 2); } png_set_keep_unknown_chunks(mPNG, 1, unused_chunks, (int)sizeof(unused_chunks)/5); #endif #ifdef PNG_SET_USER_LIMITS_SUPPORTED png_set_user_limits(mPNG, MOZ_PNG_MAX_WIDTH, MOZ_PNG_MAX_HEIGHT); if (mCMSMode != eCMSMode_Off) { png_set_chunk_malloc_max(mPNG, 4000000L); } #endif #ifdef PNG_READ_CHECK_FOR_INVALID_INDEX_SUPPORTED // Disallow palette-index checking, for speed; we would ignore the warning // anyhow. This feature was added at libpng version 1.5.10 and is disabled // in the embedded libpng but enabled by default in the system libpng. This // call also disables it in the system libpng, for decoding speed. // Bug #745202. png_set_check_for_invalid_index(mPNG, 0); #endif #ifdef PNG_SET_OPTION_SUPPORTED #if defined(PNG_sRGB_PROFILE_CHECKS) && PNG_sRGB_PROFILE_CHECKS >= 0 // Skip checking of sRGB ICC profiles png_set_option(mPNG, PNG_SKIP_sRGB_CHECK_PROFILE, PNG_OPTION_ON); #endif #ifdef PNG_MAXIMUM_INFLATE_WINDOW // Force a larger zlib inflate window as some images in the wild have // incorrectly set metadata (specifically CMF bits) which prevent us from // decoding them otherwise. png_set_option(mPNG, PNG_MAXIMUM_INFLATE_WINDOW, PNG_OPTION_ON); #endif #endif // use this as libpng "progressive pointer" (retrieve in callbacks) png_set_progressive_read_fn(mPNG, static_cast(this), nsPNGDecoder::info_callback, nsPNGDecoder::row_callback, nsPNGDecoder::end_callback); return NS_OK; } LexerResult nsPNGDecoder::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::PNG_DATA: return ReadPNGData(aData, aLength); case State::FINISHED_PNG_DATA: return FinishedPNGData(); } MOZ_CRASH("Unknown State"); }); } LexerTransition nsPNGDecoder::ReadPNGData(const char* aData, size_t aLength) { // If we were waiting until after returning from a yield to call // CreateFrame(), call it now. if (mNextFrameInfo) { if (NS_FAILED(CreateFrame(*mNextFrameInfo))) { return Transition::TerminateFailure(); } MOZ_ASSERT(mImageData, "Should have a buffer now"); mNextFrameInfo = Nothing(); } // libpng uses setjmp/longjmp for error handling. if (setjmp(png_jmpbuf(mPNG))) { return Transition::TerminateFailure(); } // Pass the data off to libpng. mLastChunkLength = aLength; mNextTransition = Transition::ContinueUnbuffered(State::PNG_DATA); png_process_data(mPNG, mInfo, reinterpret_cast(const_cast((aData))), aLength); // Make sure that we've reached a terminal state if decoding is done. MOZ_ASSERT_IF(GetDecodeDone(), mNextTransition.NextStateIsTerminal()); MOZ_ASSERT_IF(HasError(), mNextTransition.NextStateIsTerminal()); // Continue with whatever transition the callback code requested. We // initialized this to Transition::ContinueUnbuffered(State::PNG_DATA) above, // so by default we just continue the unbuffered read. return mNextTransition; } LexerTransition nsPNGDecoder::FinishedPNGData() { // 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(); } // Sets up gamma pre-correction in libpng before our callback gets called. // We need to do this if we don't end up with a CMS profile. static void PNGDoGammaCorrection(png_structp png_ptr, png_infop info_ptr) { double aGamma; if (png_get_gAMA(png_ptr, info_ptr, &aGamma)) { if ((aGamma <= 0.0) || (aGamma > 21474.83)) { aGamma = 0.45455; png_set_gAMA(png_ptr, info_ptr, aGamma); } png_set_gamma(png_ptr, 2.2, aGamma); } else { png_set_gamma(png_ptr, 2.2, 0.45455); } } // Adapted from http://www.littlecms.com/pngchrm.c example code static qcms_profile* PNGGetColorProfile(png_structp png_ptr, png_infop info_ptr, int color_type, qcms_data_type* inType, uint32_t* intent) { qcms_profile* profile = nullptr; *intent = QCMS_INTENT_PERCEPTUAL; // Our default // First try to see if iCCP chunk is present if (png_get_valid(png_ptr, info_ptr, PNG_INFO_iCCP)) { png_uint_32 profileLen; png_bytep profileData; png_charp profileName; int compression; png_get_iCCP(png_ptr, info_ptr, &profileName, &compression, &profileData, &profileLen); profile = qcms_profile_from_memory((char*)profileData, profileLen); if (profile) { uint32_t profileSpace = qcms_profile_get_color_space(profile); bool mismatch = false; if (color_type & PNG_COLOR_MASK_COLOR) { if (profileSpace != icSigRgbData) { mismatch = true; } } else { if (profileSpace == icSigRgbData) { png_set_gray_to_rgb(png_ptr); } else if (profileSpace != icSigGrayData) { mismatch = true; } } if (mismatch) { qcms_profile_release(profile); profile = nullptr; } else { *intent = qcms_profile_get_rendering_intent(profile); } } } // Check sRGB chunk if (!profile && png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) { profile = qcms_profile_sRGB(); if (profile) { int fileIntent; png_set_gray_to_rgb(png_ptr); png_get_sRGB(png_ptr, info_ptr, &fileIntent); uint32_t map[] = { QCMS_INTENT_PERCEPTUAL, QCMS_INTENT_RELATIVE_COLORIMETRIC, QCMS_INTENT_SATURATION, QCMS_INTENT_ABSOLUTE_COLORIMETRIC }; *intent = map[fileIntent]; } } // Check gAMA/cHRM chunks if (!profile && png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA) && png_get_valid(png_ptr, info_ptr, PNG_INFO_cHRM)) { qcms_CIE_xyYTRIPLE primaries; qcms_CIE_xyY whitePoint; png_get_cHRM(png_ptr, info_ptr, &whitePoint.x, &whitePoint.y, &primaries.red.x, &primaries.red.y, &primaries.green.x, &primaries.green.y, &primaries.blue.x, &primaries.blue.y); whitePoint.Y = primaries.red.Y = primaries.green.Y = primaries.blue.Y = 1.0; double gammaOfFile; png_get_gAMA(png_ptr, info_ptr, &gammaOfFile); profile = qcms_profile_create_rgb_with_gamma(whitePoint, primaries, 1.0/gammaOfFile); if (profile) { png_set_gray_to_rgb(png_ptr); } } if (profile) { uint32_t profileSpace = qcms_profile_get_color_space(profile); if (profileSpace == icSigGrayData) { if (color_type & PNG_COLOR_MASK_ALPHA) { *inType = QCMS_DATA_GRAYA_8; } else { *inType = QCMS_DATA_GRAY_8; } } else { if (color_type & PNG_COLOR_MASK_ALPHA || png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { *inType = QCMS_DATA_RGBA_8; } else { *inType = QCMS_DATA_RGB_8; } } } return profile; } void nsPNGDecoder::info_callback(png_structp png_ptr, png_infop info_ptr) { png_uint_32 width, height; int bit_depth, color_type, interlace_type, compression_type, filter_type; unsigned int channels; png_bytep trans = nullptr; int num_trans = 0; nsPNGDecoder* decoder = static_cast(png_get_progressive_ptr(png_ptr)); // Always decode to 24-bit RGB or 32-bit RGBA png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, &interlace_type, &compression_type, &filter_type); const IntRect frameRect(0, 0, width, height); // Post our size to the superclass decoder->PostSize(frameRect.width, frameRect.height); if (decoder->HasError()) { // Setting the size led to an error. png_error(decoder->mPNG, "Sizing error"); } if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_expand(png_ptr); } if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { png_set_expand(png_ptr); } if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { png_color_16p trans_values; png_get_tRNS(png_ptr, info_ptr, &trans, &num_trans, &trans_values); // libpng doesn't reject a tRNS chunk with out-of-range samples // so we check it here to avoid setting up a useless opacity // channel or producing unexpected transparent pixels (bug #428045) if (bit_depth < 16) { png_uint_16 sample_max = (1 << bit_depth) - 1; if ((color_type == PNG_COLOR_TYPE_GRAY && trans_values->gray > sample_max) || (color_type == PNG_COLOR_TYPE_RGB && (trans_values->red > sample_max || trans_values->green > sample_max || trans_values->blue > sample_max))) { // clear the tRNS valid flag and release tRNS memory png_free_data(png_ptr, info_ptr, PNG_FREE_TRNS, 0); num_trans = 0; } } if (num_trans != 0) { png_set_expand(png_ptr); } } if (bit_depth == 16) { png_set_scale_16(png_ptr); } qcms_data_type inType = QCMS_DATA_RGBA_8; uint32_t intent = -1; uint32_t pIntent; if (decoder->mCMSMode != eCMSMode_Off) { intent = gfxPlatform::GetRenderingIntent(); decoder->mInProfile = PNGGetColorProfile(png_ptr, info_ptr, color_type, &inType, &pIntent); // If we're not mandating an intent, use the one from the image. if (intent == uint32_t(-1)) { intent = pIntent; } } if (decoder->mInProfile && gfxPlatform::GetCMSOutputProfile()) { qcms_data_type outType; if (color_type & PNG_COLOR_MASK_ALPHA || num_trans) { outType = QCMS_DATA_RGBA_8; } else { outType = QCMS_DATA_RGB_8; } decoder->mTransform = qcms_transform_create(decoder->mInProfile, inType, gfxPlatform::GetCMSOutputProfile(), outType, (qcms_intent)intent); } else { png_set_gray_to_rgb(png_ptr); // only do gamma correction if CMS isn't entirely disabled if (decoder->mCMSMode != eCMSMode_Off) { PNGDoGammaCorrection(png_ptr, info_ptr); } if (decoder->mCMSMode == eCMSMode_All) { if (color_type & PNG_COLOR_MASK_ALPHA || num_trans) { decoder->mTransform = gfxPlatform::GetCMSRGBATransform(); } else { decoder->mTransform = gfxPlatform::GetCMSRGBTransform(); } } } // Let libpng expand interlaced images. const bool isInterlaced = interlace_type == PNG_INTERLACE_ADAM7; if (isInterlaced) { png_set_interlace_handling(png_ptr); } // now all of those things we set above are used to update various struct // members and whatnot, after which we can get channels, rowbytes, etc. png_read_update_info(png_ptr, info_ptr); decoder->mChannels = channels = png_get_channels(png_ptr, info_ptr); //---------------------------------------------------------------// // copy PNG info into imagelib structs (formerly png_set_dims()) // //---------------------------------------------------------------// if (channels == 1 || channels == 3) { decoder->format = gfx::SurfaceFormat::B8G8R8X8; } else if (channels == 2 || channels == 4) { decoder->format = gfx::SurfaceFormat::B8G8R8A8; } else { png_error(decoder->mPNG, "Invalid number of channels"); } #ifdef PNG_APNG_SUPPORTED bool isAnimated = png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL); if (isAnimated) { int32_t rawTimeout = GetNextFrameDelay(png_ptr, info_ptr); decoder->PostIsAnimated(FrameTimeout::FromRawMilliseconds(rawTimeout)); if (decoder->Size() != decoder->OutputSize() && !decoder->IsFirstFrameDecode()) { MOZ_ASSERT_UNREACHABLE("Doing downscale-during-decode " "for an animated image?"); png_error(decoder->mPNG, "Invalid downscale attempt"); // Abort decode. } } #endif if (decoder->IsMetadataDecode()) { // If we are animated then the first frame rect is either: // 1) the whole image if the IDAT chunk is part of the animation // 2) the frame rect of the first fDAT chunk otherwise. // If we are not animated then we want to make sure to call // PostHasTransparency in the metadata decode if we need to. So it's // okay to pass IntRect(0, 0, width, height) here for animated images; // they will call with the proper first frame rect in the full decode. auto transparency = decoder->GetTransparencyType(decoder->format, frameRect); decoder->PostHasTransparencyIfNeeded(transparency); // We have the metadata we're looking for, so stop here, before we allocate // buffers below. return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); } #ifdef PNG_APNG_SUPPORTED if (isAnimated) { png_set_progressive_frame_fn(png_ptr, nsPNGDecoder::frame_info_callback, nullptr); } if (png_get_first_frame_is_hidden(png_ptr, info_ptr)) { decoder->mFrameIsHidden = true; } else { #endif nsresult rv = decoder->CreateFrame(FrameInfo{ decoder->format, frameRect, isInterlaced }); if (NS_FAILED(rv)) { png_error(decoder->mPNG, "CreateFrame failed"); } MOZ_ASSERT(decoder->mImageData, "Should have a buffer now"); #ifdef PNG_APNG_SUPPORTED } #endif if (decoder->mTransform && (channels <= 2 || isInterlaced)) { uint32_t bpp[] = { 0, 3, 4, 3, 4 }; decoder->mCMSLine = static_cast(malloc(bpp[channels] * frameRect.width)); if (!decoder->mCMSLine) { png_error(decoder->mPNG, "malloc of mCMSLine failed"); } } if (interlace_type == PNG_INTERLACE_ADAM7) { if (frameRect.height < INT32_MAX / (frameRect.width * int32_t(channels))) { const size_t bufferSize = channels * frameRect.width * frameRect.height; decoder->interlacebuf = static_cast(malloc(bufferSize)); } if (!decoder->interlacebuf) { png_error(decoder->mPNG, "malloc of interlacebuf failed"); } } } void nsPNGDecoder::PostInvalidationIfNeeded() { Maybe invalidRect = mPipe.TakeInvalidRect(); if (!invalidRect) { return; } PostInvalidation(invalidRect->mInputSpaceRect, Some(invalidRect->mOutputSpaceRect)); } static NextPixel PackRGBPixelAndAdvance(uint8_t*& aRawPixelInOut) { const uint32_t pixel = gfxPackedPixel(0xFF, aRawPixelInOut[0], aRawPixelInOut[1], aRawPixelInOut[2]); aRawPixelInOut += 3; return AsVariant(pixel); } static NextPixel PackRGBAPixelAndAdvance(uint8_t*& aRawPixelInOut) { const uint32_t pixel = gfxPackedPixel(aRawPixelInOut[3], aRawPixelInOut[0], aRawPixelInOut[1], aRawPixelInOut[2]); aRawPixelInOut += 4; return AsVariant(pixel); } static NextPixel PackUnpremultipliedRGBAPixelAndAdvance(uint8_t*& aRawPixelInOut) { const uint32_t pixel = gfxPackedPixelNoPreMultiply(aRawPixelInOut[3], aRawPixelInOut[0], aRawPixelInOut[1], aRawPixelInOut[2]); aRawPixelInOut += 4; return AsVariant(pixel); } void nsPNGDecoder::row_callback(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num, int pass) { /* libpng comments: * * This function is called for every row in the image. If the * image is interlacing, and you turned on the interlace handler, * this function will be called for every row in every pass. * Some of these rows will not be changed from the previous pass. * When the row is not changed, the new_row variable will be * nullptr. The rows and passes are called in order, so you don't * really need the row_num and pass, but I'm supplying them * because it may make your life easier. * * For the non-nullptr rows of interlaced images, you must call * png_progressive_combine_row() passing in the row and the * old row. You can call this function for nullptr rows (it will * just return) and for non-interlaced images (it just does the * memcpy for you) if it will make the code easier. Thus, you * can just do this for all cases: * * png_progressive_combine_row(png_ptr, old_row, new_row); * * where old_row is what was displayed for previous rows. Note * that the first pass (pass == 0 really) will completely cover * the old row, so the rows do not have to be initialized. After * the first pass (and only for interlaced images), you will have * to pass the current row, and the function will combine the * old row and the new row. */ nsPNGDecoder* decoder = static_cast(png_get_progressive_ptr(png_ptr)); if (decoder->mFrameIsHidden) { return; // Skip this frame. } MOZ_ASSERT_IF(decoder->IsFirstFrameDecode(), decoder->mNumFrames == 0); while (pass > decoder->mPass) { // Advance to the next pass. We may have to do this multiple times because // libpng will skip passes if the image is so small that no pixels have // changed on a given pass, but ADAM7InterpolatingFilter needs to be reset // once for every pass to perform interpolation properly. decoder->mPipe.ResetToFirstRow(); decoder->mPass++; } const png_uint_32 height = static_cast(decoder->mFrameRect.height); if (row_num >= height) { // Bail if we receive extra rows. This is especially important because if we // didn't, we might overflow the deinterlacing buffer. MOZ_ASSERT_UNREACHABLE("libpng producing extra rows?"); return; } // Note that |new_row| may be null here, indicating that this is an interlaced // image and |row_callback| is being called for a row that hasn't changed. MOZ_ASSERT_IF(!new_row, decoder->interlacebuf); uint8_t* rowToWrite = new_row; if (decoder->interlacebuf) { uint32_t width = uint32_t(decoder->mFrameRect.width); // We'll output the deinterlaced version of the row. rowToWrite = decoder->interlacebuf + (row_num * decoder->mChannels * width); // Update the deinterlaced version of this row with the new data. png_progressive_combine_row(png_ptr, rowToWrite, new_row); } decoder->WriteRow(rowToWrite); } void nsPNGDecoder::WriteRow(uint8_t* aRow) { MOZ_ASSERT(aRow); uint8_t* rowToWrite = aRow; uint32_t width = uint32_t(mFrameRect.width); // Apply color management to the row, if necessary, before writing it out. if (mTransform) { if (mCMSLine) { qcms_transform_data(mTransform, rowToWrite, mCMSLine, width); // Copy alpha over. if (mChannels == 2 || mChannels == 4) { for (uint32_t i = 0; i < width; ++i) { mCMSLine[4 * i + 3] = rowToWrite[mChannels * i + mChannels - 1]; } } rowToWrite = mCMSLine; } else { qcms_transform_data(mTransform, rowToWrite, rowToWrite, width); } } // Write this row to the SurfacePipe. DebugOnly result = WriteState::FAILURE; switch (format) { case SurfaceFormat::B8G8R8X8: result = mPipe.WritePixelsToRow([&]{ return PackRGBPixelAndAdvance(rowToWrite); }); break; case SurfaceFormat::B8G8R8A8: if (mDisablePremultipliedAlpha) { result = mPipe.WritePixelsToRow([&]{ return PackUnpremultipliedRGBAPixelAndAdvance(rowToWrite); }); } else { result = mPipe.WritePixelsToRow([&]{ return PackRGBAPixelAndAdvance(rowToWrite); }); } break; default: png_error(mPNG, "Invalid SurfaceFormat"); } MOZ_ASSERT(WriteState(result) != WriteState::FAILURE); PostInvalidationIfNeeded(); } void nsPNGDecoder::DoTerminate(png_structp aPNGStruct, TerminalState aState) { // Stop processing data. Note that we intentionally ignore the return value of // png_process_data_pause(), which tells us how many bytes of the data that // was passed to png_process_data() have not been consumed yet, because now // that we've reached a terminal state, we won't do any more decoding or call // back into libpng anymore. png_process_data_pause(aPNGStruct, /* save = */ false); mNextTransition = aState == TerminalState::SUCCESS ? Transition::TerminateSuccess() : Transition::TerminateFailure(); } void nsPNGDecoder::DoYield(png_structp aPNGStruct) { // Pause data processing. png_process_data_pause() returns how many bytes of // the data that was passed to png_process_data() have not been consumed yet. // We use this information to tell StreamingLexer where to place us in the // input stream when we come back from the yield. png_size_t pendingBytes = png_process_data_pause(aPNGStruct, /* save = */ false); MOZ_ASSERT(pendingBytes < mLastChunkLength); size_t consumedBytes = mLastChunkLength - min(pendingBytes, mLastChunkLength); mNextTransition = Transition::ContinueUnbufferedAfterYield(State::PNG_DATA, consumedBytes); } #ifdef PNG_APNG_SUPPORTED // got the header of a new frame that's coming void nsPNGDecoder::frame_info_callback(png_structp png_ptr, png_uint_32 frame_num) { nsPNGDecoder* decoder = static_cast(png_get_progressive_ptr(png_ptr)); // old frame is done decoder->EndImageFrame(); const bool previousFrameWasHidden = decoder->mFrameIsHidden; if (!previousFrameWasHidden && decoder->IsFirstFrameDecode()) { // We're about to get a second non-hidden frame, but we only want the first. // Stop decoding now. (And avoid allocating the unnecessary buffers below.) decoder->PostDecodeDone(); return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); } // Only the first frame can be hidden, so unhide unconditionally here. decoder->mFrameIsHidden = false; // Save the information necessary to create the frame; we'll actually create // it when we return from the yield. const IntRect frameRect(png_get_next_frame_x_offset(png_ptr, decoder->mInfo), png_get_next_frame_y_offset(png_ptr, decoder->mInfo), png_get_next_frame_width(png_ptr, decoder->mInfo), png_get_next_frame_height(png_ptr, decoder->mInfo)); const bool isInterlaced = bool(decoder->interlacebuf); #ifndef MOZ_EMBEDDED_LIBPNG // if using system library, check frame_width and height against 0 if (frameRect.width == 0) { png_error(png_ptr, "Frame width must not be 0"); } if (frameRect.height == 0) { png_error(png_ptr, "Frame height must not be 0"); } #endif const FrameInfo info { decoder->format, frameRect, isInterlaced }; // If the previous frame was hidden, skip the yield (which will mislead the // caller, who will think the previous frame was real) and just allocate the // new frame here. if (previousFrameWasHidden) { if (NS_FAILED(decoder->CreateFrame(info))) { return decoder->DoTerminate(png_ptr, TerminalState::FAILURE); } MOZ_ASSERT(decoder->mImageData, "Should have a buffer now"); return; // No yield, so we'll just keep decoding. } // Yield to the caller to notify them that the previous frame is now complete. decoder->mNextFrameInfo = Some(info); return decoder->DoYield(png_ptr); } #endif void nsPNGDecoder::end_callback(png_structp png_ptr, png_infop info_ptr) { /* libpng comments: * * this function is called when the whole image has been read, * including any chunks after the image (up to and including * the IEND). You will usually have the same info chunk as you * had in the header, although some data may have been added * to the comments and time fields. * * Most people won't do much here, perhaps setting a flag that * marks the image as finished. */ nsPNGDecoder* decoder = static_cast(png_get_progressive_ptr(png_ptr)); // We shouldn't get here if we've hit an error MOZ_ASSERT(!decoder->HasError(), "Finishing up PNG but hit error!"); int32_t loop_count = 0; #ifdef PNG_APNG_SUPPORTED if (png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL)) { int32_t num_plays = png_get_num_plays(png_ptr, info_ptr); loop_count = num_plays - 1; } #endif // Send final notifications. decoder->EndImageFrame(); decoder->PostDecodeDone(loop_count); return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS); } void nsPNGDecoder::error_callback(png_structp png_ptr, png_const_charp error_msg) { MOZ_LOG(sPNGLog, LogLevel::Error, ("libpng error: %s\n", error_msg)); png_longjmp(png_ptr, 1); } void nsPNGDecoder::warning_callback(png_structp png_ptr, png_const_charp warning_msg) { MOZ_LOG(sPNGLog, LogLevel::Warning, ("libpng warning: %s\n", warning_msg)); } Maybe nsPNGDecoder::SpeedHistogram() const { return Some(Telemetry::IMAGE_DECODE_SPEED_PNG); } bool nsPNGDecoder::IsValidICO() const { // Only 32-bit RGBA PNGs are valid ICO resources; see here: // http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx // If there are errors in the call to png_get_IHDR, the error_callback in // nsPNGDecoder.cpp is called. In this error callback we do a longjmp, so // we need to save the jump buffer here. Otherwise we'll end up without a // proper callstack. if (setjmp(png_jmpbuf(mPNG))) { // We got here from a longjmp call indirectly from png_get_IHDR return false; } png_uint_32 png_width, // Unused png_height; // Unused int png_bit_depth, png_color_type; if (png_get_IHDR(mPNG, mInfo, &png_width, &png_height, &png_bit_depth, &png_color_type, nullptr, nullptr, nullptr)) { return ((png_color_type == PNG_COLOR_TYPE_RGB_ALPHA || png_color_type == PNG_COLOR_TYPE_RGB) && png_bit_depth == 8); } else { return false; } } } // namespace image } // namespace mozilla