diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /image/decoders/nsGIFDecoder2.cpp | |
parent | Initial commit. (diff) | |
download | firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'image/decoders/nsGIFDecoder2.cpp')
-rw-r--r-- | image/decoders/nsGIFDecoder2.cpp | 1073 |
1 files changed, 1073 insertions, 0 deletions
diff --git a/image/decoders/nsGIFDecoder2.cpp b/image/decoders/nsGIFDecoder2.cpp new file mode 100644 index 0000000000..9b2de9124a --- /dev/null +++ b/image/decoders/nsGIFDecoder2.cpp @@ -0,0 +1,1073 @@ +/* -*- 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/. */ +/* +The Graphics Interchange Format(c) is the copyright property of CompuServe +Incorporated. Only CompuServe Incorporated is authorized to define, redefine, +enhance, alter, modify or change in any way the definition of the format. + +CompuServe Incorporated hereby grants a limited, non-exclusive, royalty-free +license for the use of the Graphics Interchange Format(sm) in computer +software; computer software utilizing GIF(sm) must acknowledge ownership of the +Graphics Interchange Format and its Service Mark by CompuServe Incorporated, in +User and Technical Documentation. Computer software utilizing GIF, which is +distributed or may be distributed without User or Technical Documentation must +display to the screen or printer a message acknowledging ownership of the +Graphics Interchange Format and the Service Mark by CompuServe Incorporated; in +this case, the acknowledgement may be displayed in an opening screen or leading +banner, or a closing screen or trailing banner. A message such as the following +may be used: + + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +For further information, please contact : + + CompuServe Incorporated + Graphics Technology Department + 5000 Arlington Center Boulevard + Columbus, Ohio 43220 + U. S. A. + +CompuServe Incorporated maintains a mailing list with all those individuals and +organizations who wish to receive copies of this document when it is corrected +or revised. This service is offered free of charge; please provide us with your +mailing address. +*/ + +#include "nsGIFDecoder2.h" + +#include <stddef.h> + +#include "imgFrame.h" +#include "mozilla/EndianUtils.h" +#include "RasterImage.h" +#include "SurfacePipeFactory.h" + +#include "gfxColor.h" +#include "gfxPlatform.h" +#include "qcms.h" +#include <algorithm> +#include "mozilla/Telemetry.h" + +using namespace mozilla::gfx; + +using std::max; + +namespace mozilla { +namespace image { + +////////////////////////////////////////////////////////////////////// +// GIF Decoder Implementation + +static const size_t GIF_HEADER_LEN = 6; +static const size_t GIF_SCREEN_DESCRIPTOR_LEN = 7; +static const size_t BLOCK_HEADER_LEN = 1; +static const size_t SUB_BLOCK_HEADER_LEN = 1; +static const size_t EXTENSION_HEADER_LEN = 2; +static const size_t GRAPHIC_CONTROL_EXTENSION_LEN = 4; +static const size_t APPLICATION_EXTENSION_LEN = 11; +static const size_t IMAGE_DESCRIPTOR_LEN = 9; + +// Masks for reading color table information from packed fields in the screen +// descriptor and image descriptor blocks. +static const uint8_t PACKED_FIELDS_COLOR_TABLE_BIT = 0x80; +static const uint8_t PACKED_FIELDS_INTERLACED_BIT = 0x40; +static const uint8_t PACKED_FIELDS_TABLE_DEPTH_MASK = 0x07; + +nsGIFDecoder2::nsGIFDecoder2(RasterImage* aImage) + : Decoder(aImage), + mLexer(Transition::To(State::GIF_HEADER, GIF_HEADER_LEN), + Transition::TerminateSuccess()), + mOldColor(0), + mCurrentFrameIndex(-1), + mColorTablePos(0), + mColormap(nullptr), + mColormapSize(0), + mColorMask('\0'), + mGIFOpen(false), + mSawTransparency(false), + mSwizzleFn(nullptr) { + // Clear out the structure, excluding the arrays. Ensure that the global + // colormap is initialized as opaque. + memset(&mGIFStruct, 0, sizeof(mGIFStruct)); + memset(mGIFStruct.global_colormap, 0xFF, sizeof(mGIFStruct.global_colormap)); + + // Each color table will need to be unpacked. + mSwizzleFn = SwizzleRow(SurfaceFormat::R8G8B8, SurfaceFormat::OS_RGBA); + MOZ_ASSERT(mSwizzleFn); +} + +nsGIFDecoder2::~nsGIFDecoder2() { free(mGIFStruct.local_colormap); } + +nsresult nsGIFDecoder2::FinishInternal() { + MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!"); + + // If the GIF got cut off, handle it anyway + if (!IsMetadataDecode() && mGIFOpen) { + if (mCurrentFrameIndex == mGIFStruct.images_decoded) { + EndImageFrame(); + } + PostDecodeDone(mGIFStruct.loop_count); + mGIFOpen = false; + } + + return NS_OK; +} + +void nsGIFDecoder2::FlushImageData() { + Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect(); + if (!invalidRect) { + return; + } + + PostInvalidation(invalidRect->mInputSpaceRect, + Some(invalidRect->mOutputSpaceRect)); +} + +//****************************************************************************** +// GIF decoder callback methods. Part of public API for GIF2 +//****************************************************************************** + +//****************************************************************************** +void nsGIFDecoder2::BeginGIF() { + if (mGIFOpen) { + return; + } + + mGIFOpen = true; + + PostSize(mGIFStruct.screen_width, mGIFStruct.screen_height); +} + +bool nsGIFDecoder2::CheckForTransparency(const OrientedIntRect& aFrameRect) { + // Check if the image has a transparent color in its palette. + if (mGIFStruct.is_transparent) { + PostHasTransparency(); + return true; + } + + // This is a bit of a hack. Some sites will use a 1x1 gif that includes no + // header information indicating it is transparent, no palette, and no image + // data at all (so no pixels get written) to represent a transparent pixel + // using the absolute least number of bytes. Generally things are setup to + // detect transparency without decoding the image data. So to detect this kind + // of transparency without decoing the image data we would have to assume + // every gif is transparent, which we would like to avoid. Changing things so + // that we can detect transparency at any point of decoding is a bigger change + // and not worth it for one questionable 1x1 gif. Using this "trick" for + // anything but 1x1 transparent spacer gifs doesn't make sense, so it's + // reasonable to target 1x1 gifs just for this. + if (mGIFStruct.screen_width == 1 && mGIFStruct.screen_height == 1) { + PostHasTransparency(); + return true; + } + + if (mGIFStruct.images_decoded > 0) { + return false; // We only care about first frame padding below. + } + + // If we need padding on the first frame, that means we don't draw into part + // of the image at all. Report that as transparency. + OrientedIntRect imageRect(0, 0, mGIFStruct.screen_width, + mGIFStruct.screen_height); + if (!imageRect.IsEqualEdges(aFrameRect)) { + PostHasTransparency(); + mSawTransparency = true; // Make sure we don't optimize it away. + return true; + } + + return false; +} + +//****************************************************************************** +nsresult nsGIFDecoder2::BeginImageFrame(const OrientedIntRect& aFrameRect, + uint16_t aDepth, bool aIsInterlaced) { + MOZ_ASSERT(HasSize()); + + bool hasTransparency = CheckForTransparency(aFrameRect); + + // Make sure there's no animation if we're downscaling. + MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation()); + + Maybe<AnimationParams> animParams; + if (!IsFirstFrameDecode()) { + animParams.emplace(aFrameRect.ToUnknownRect(), + FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time), + uint32_t(mGIFStruct.images_decoded), BlendMethod::OVER, + DisposalMethod(mGIFStruct.disposal_method)); + } + + SurfacePipeFlags pipeFlags = + aIsInterlaced ? SurfacePipeFlags::DEINTERLACE : SurfacePipeFlags(); + + gfx::SurfaceFormat format; + if (mGIFStruct.images_decoded == 0) { + // The first frame may be displayed progressively. + pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; + + // Only allow opaque surfaces if we are decoding a single image without + // transparency. For an animation, there isn't much benefit to RGBX given + // the current frame is constantly changing, and there are many risks + // since BlendAnimationFilter is able to clear rows of data. + format = hasTransparency || animParams ? SurfaceFormat::OS_RGBA + : SurfaceFormat::OS_RGBX; + } else { + format = SurfaceFormat::OS_RGBA; + } + + Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateSurfacePipe( + this, Size(), OutputSize(), aFrameRect, format, format, animParams, + mTransform, pipeFlags); + mCurrentFrameIndex = mGIFStruct.images_decoded; + + if (!pipe) { + mPipe = SurfacePipe(); + return NS_ERROR_FAILURE; + } + + mPipe = std::move(*pipe); + return NS_OK; +} + +//****************************************************************************** +void nsGIFDecoder2::EndImageFrame() { + Opacity opacity = Opacity::SOME_TRANSPARENCY; + + if (mGIFStruct.images_decoded == 0) { + // We need to send invalidations for the first frame. + FlushImageData(); + + // The first frame was preallocated with alpha; if it wasn't transparent, we + // should fix that. We can also mark it opaque unconditionally if we didn't + // actually see any transparent pixels - this test is only valid for the + // first frame. + if (!mGIFStruct.is_transparent && !mSawTransparency) { + opacity = Opacity::FULLY_OPAQUE; + } + } + + // Unconditionally increment images_decoded, because we unconditionally + // append frames in BeginImageFrame(). This ensures that images_decoded + // always refers to the frame in mImage we're currently decoding, + // even if some of them weren't decoded properly and thus are blank. + mGIFStruct.images_decoded++; + + // Reset graphic control extension parameters that we shouldn't reuse + // between frames. + mGIFStruct.delay_time = 0; + + // Tell the superclass we finished a frame + PostFrameStop(opacity); + + // Reset the transparent pixel + if (mOldColor) { + mColormap[mGIFStruct.tpixel] = mOldColor; + mOldColor = 0; + } + + mColormap = nullptr; + mColormapSize = 0; + mCurrentFrameIndex = -1; +} + +template <typename PixelSize> +PixelSize nsGIFDecoder2::ColormapIndexToPixel(uint8_t aIndex) { + MOZ_ASSERT(sizeof(PixelSize) == sizeof(uint32_t)); + + // Retrieve the next color, clamping to the size of the colormap. + uint32_t color = mColormap[aIndex & mColorMask]; + + // Check for transparency. + if (mGIFStruct.is_transparent) { + mSawTransparency = mSawTransparency || color == 0; + } + + return color; +} + +template <> +uint8_t nsGIFDecoder2::ColormapIndexToPixel<uint8_t>(uint8_t aIndex) { + return aIndex & mColorMask; +} + +template <typename PixelSize> +std::tuple<int32_t, Maybe<WriteState>> nsGIFDecoder2::YieldPixels( + const uint8_t* aData, size_t aLength, size_t* aBytesReadOut, + PixelSize* aPixelBlock, int32_t aBlockSize) { + MOZ_ASSERT(aData); + MOZ_ASSERT(aBytesReadOut); + MOZ_ASSERT(mGIFStruct.stackp >= mGIFStruct.stack); + + // Advance to the next byte we should read. + const uint8_t* data = aData + *aBytesReadOut; + + int32_t written = 0; + while (aBlockSize > written) { + // If we don't have any decoded data to yield, try to read some input and + // produce some. + if (mGIFStruct.stackp == mGIFStruct.stack) { + while (mGIFStruct.bits < mGIFStruct.codesize && + *aBytesReadOut < aLength) { + // Feed the next byte into the decoder's 32-bit input buffer. + mGIFStruct.datum += int32_t(*data) << mGIFStruct.bits; + mGIFStruct.bits += 8; + data += 1; + *aBytesReadOut += 1; + } + + if (mGIFStruct.bits < mGIFStruct.codesize) { + return std::make_tuple(written, Some(WriteState::NEED_MORE_DATA)); + } + + // Get the leading variable-length symbol from the data stream. + int code = mGIFStruct.datum & mGIFStruct.codemask; + mGIFStruct.datum >>= mGIFStruct.codesize; + mGIFStruct.bits -= mGIFStruct.codesize; + + const int clearCode = ClearCode(); + + // Reset the dictionary to its original state, if requested + if (code == clearCode) { + mGIFStruct.codesize = mGIFStruct.datasize + 1; + mGIFStruct.codemask = (1 << mGIFStruct.codesize) - 1; + mGIFStruct.avail = clearCode + 2; + mGIFStruct.oldcode = -1; + return std::make_tuple(written, Some(WriteState::NEED_MORE_DATA)); + } + + // Check for explicit end-of-stream code. It should only appear after all + // image data, but if that was the case we wouldn't be in this function, + // so this is always an error condition. + if (code == (clearCode + 1)) { + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + if (mGIFStruct.oldcode == -1) { + if (code >= MAX_BITS) { + // The code's too big; something's wrong. + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + mGIFStruct.firstchar = mGIFStruct.oldcode = code; + + // Yield a pixel at the appropriate index in the colormap. + mGIFStruct.pixels_remaining--; + aPixelBlock[written++] = + ColormapIndexToPixel<PixelSize>(mGIFStruct.suffix[code]); + continue; + } + + int incode = code; + if (code >= mGIFStruct.avail) { + *mGIFStruct.stackp++ = mGIFStruct.firstchar; + code = mGIFStruct.oldcode; + + if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) { + // Stack overflow; something's wrong. + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + } + + while (code >= clearCode) { + if ((code >= MAX_BITS) || (code == mGIFStruct.prefix[code])) { + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + *mGIFStruct.stackp++ = mGIFStruct.suffix[code]; + code = mGIFStruct.prefix[code]; + + if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) { + // Stack overflow; something's wrong. + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + } + + *mGIFStruct.stackp++ = mGIFStruct.firstchar = mGIFStruct.suffix[code]; + + // Define a new codeword in the dictionary. + if (mGIFStruct.avail < 4096) { + mGIFStruct.prefix[mGIFStruct.avail] = mGIFStruct.oldcode; + mGIFStruct.suffix[mGIFStruct.avail] = mGIFStruct.firstchar; + mGIFStruct.avail++; + + // If we've used up all the codewords of a given length increase the + // length of codewords by one bit, but don't exceed the specified + // maximum codeword size of 12 bits. + if (((mGIFStruct.avail & mGIFStruct.codemask) == 0) && + (mGIFStruct.avail < 4096)) { + mGIFStruct.codesize++; + mGIFStruct.codemask += mGIFStruct.avail; + } + } + + mGIFStruct.oldcode = incode; + } + + if (MOZ_UNLIKELY(mGIFStruct.stackp <= mGIFStruct.stack)) { + MOZ_ASSERT_UNREACHABLE("No decoded data but we didn't return early?"); + return std::make_tuple(written, Some(WriteState::FAILURE)); + } + + // Yield a pixel at the appropriate index in the colormap. + mGIFStruct.pixels_remaining--; + aPixelBlock[written++] = + ColormapIndexToPixel<PixelSize>(*--mGIFStruct.stackp); + } + + return std::make_tuple(written, Maybe<WriteState>()); +} + +/// Expand the colormap from RGB to Packed ARGB as needed by Cairo. +/// And apply any LCMS transformation. +void nsGIFDecoder2::ConvertColormap(uint32_t* aColormap, uint32_t aColors) { + if (!aColors) { + return; + } + + // Apply CMS transformation if enabled and available + if (mCMSMode == CMSMode::All) { + qcms_transform* transform = GetCMSsRGBTransform(SurfaceFormat::R8G8B8); + if (transform) { + qcms_transform_data(transform, aColormap, aColormap, aColors); + } + } + + // Expand color table from RGB to BGRA. + MOZ_ASSERT(mSwizzleFn); + uint8_t* data = reinterpret_cast<uint8_t*>(aColormap); + mSwizzleFn(data, data, aColors); +} + +LexerResult nsGIFDecoder2::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::GIF_HEADER: + return ReadGIFHeader(aData); + case State::SCREEN_DESCRIPTOR: + return ReadScreenDescriptor(aData); + case State::GLOBAL_COLOR_TABLE: + return ReadGlobalColorTable(aData, aLength); + case State::FINISHED_GLOBAL_COLOR_TABLE: + return FinishedGlobalColorTable(); + case State::BLOCK_HEADER: + return ReadBlockHeader(aData); + case State::EXTENSION_HEADER: + return ReadExtensionHeader(aData); + case State::GRAPHIC_CONTROL_EXTENSION: + return ReadGraphicControlExtension(aData); + case State::APPLICATION_IDENTIFIER: + return ReadApplicationIdentifier(aData); + case State::NETSCAPE_EXTENSION_SUB_BLOCK: + return ReadNetscapeExtensionSubBlock(aData); + case State::NETSCAPE_EXTENSION_DATA: + return ReadNetscapeExtensionData(aData); + case State::IMAGE_DESCRIPTOR: + return ReadImageDescriptor(aData); + case State::FINISH_IMAGE_DESCRIPTOR: + return FinishImageDescriptor(aData); + case State::LOCAL_COLOR_TABLE: + return ReadLocalColorTable(aData, aLength); + case State::FINISHED_LOCAL_COLOR_TABLE: + return FinishedLocalColorTable(); + case State::IMAGE_DATA_BLOCK: + return ReadImageDataBlock(aData); + case State::IMAGE_DATA_SUB_BLOCK: + return ReadImageDataSubBlock(aData); + case State::LZW_DATA: + return ReadLZWData(aData, aLength); + case State::SKIP_LZW_DATA: + return Transition::ContinueUnbuffered(State::SKIP_LZW_DATA); + case State::FINISHED_LZW_DATA: + return Transition::To(State::IMAGE_DATA_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + case State::SKIP_SUB_BLOCKS: + return SkipSubBlocks(aData); + case State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS: + return Transition::ContinueUnbuffered( + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS); + case State::FINISHED_SKIPPING_DATA: + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); + default: + MOZ_CRASH("Unknown State"); + } + }); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadGIFHeader( + const char* aData) { + // We retrieve the version here but because many GIF encoders set header + // fields incorrectly, we barely use it; features which should only appear in + // GIF89a are always accepted. + if (strncmp(aData, "GIF87a", GIF_HEADER_LEN) == 0) { + mGIFStruct.version = 87; + } else if (strncmp(aData, "GIF89a", GIF_HEADER_LEN) == 0) { + mGIFStruct.version = 89; + } else { + return Transition::TerminateFailure(); + } + + return Transition::To(State::SCREEN_DESCRIPTOR, GIF_SCREEN_DESCRIPTOR_LEN); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadScreenDescriptor( + const char* aData) { + mGIFStruct.screen_width = LittleEndian::readUint16(aData + 0); + mGIFStruct.screen_height = LittleEndian::readUint16(aData + 2); + + const uint8_t packedFields = aData[4]; + + // XXX: Should we be capturing these values even if there is no global color + // table? + mGIFStruct.global_colormap_depth = + (packedFields & PACKED_FIELDS_TABLE_DEPTH_MASK) + 1; + mGIFStruct.global_colormap_count = 1 << mGIFStruct.global_colormap_depth; + + // We ignore several fields in the header. We don't care about the 'sort + // flag', which indicates if the global color table's entries are sorted in + // order of importance - if we need to render this image for a device with a + // narrower color gamut than GIF supports we'll handle that at a different + // layer. We have no use for the pixel aspect ratio as well. Finally, we + // intentionally ignore the background color index, as implementing that + // feature would not be web compatible - when a GIF image frame doesn't cover + // the entire area of the image, the area that's not covered should always be + // transparent. + + if (packedFields & PACKED_FIELDS_COLOR_TABLE_BIT) { + MOZ_ASSERT(mColorTablePos == 0); + + // We read the global color table in unbuffered mode since it can be quite + // large and it'd be preferable to avoid unnecessary copies. + const size_t globalColorTableSize = 3 * mGIFStruct.global_colormap_count; + return Transition::ToUnbuffered(State::FINISHED_GLOBAL_COLOR_TABLE, + State::GLOBAL_COLOR_TABLE, + globalColorTableSize); + } + + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadGlobalColorTable( + const char* aData, size_t aLength) { + uint8_t* dest = + reinterpret_cast<uint8_t*>(mGIFStruct.global_colormap) + mColorTablePos; + memcpy(dest, aData, aLength); + mColorTablePos += aLength; + return Transition::ContinueUnbuffered(State::GLOBAL_COLOR_TABLE); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::FinishedGlobalColorTable() { + ConvertColormap(mGIFStruct.global_colormap, mGIFStruct.global_colormap_count); + mColorTablePos = 0; + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadBlockHeader( + const char* aData) { + // Determine what type of block we're dealing with. + switch (aData[0]) { + case GIF_EXTENSION_INTRODUCER: + return Transition::To(State::EXTENSION_HEADER, EXTENSION_HEADER_LEN); + + case GIF_IMAGE_SEPARATOR: + return Transition::To(State::IMAGE_DESCRIPTOR, IMAGE_DESCRIPTOR_LEN); + + case GIF_TRAILER: + FinishInternal(); + return Transition::TerminateSuccess(); + + default: + // If we get anything other than GIF_IMAGE_SEPARATOR, + // GIF_EXTENSION_INTRODUCER, or GIF_TRAILER, there is extraneous data + // between blocks. The GIF87a spec tells us to keep reading until we find + // an image separator, but GIF89a says such a file is corrupt. We follow + // GIF89a and bail out. + + if (mGIFStruct.images_decoded > 0) { + // The file is corrupt, but we successfully decoded some frames, so we + // may as well consider the decode successful and display them. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + // No images decoded; there is nothing to display. + return Transition::TerminateFailure(); + } +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadExtensionHeader( + const char* aData) { + const uint8_t label = aData[0]; + const uint8_t extensionHeaderLength = aData[1]; + + // If the extension header is zero length, just treat it as a block terminator + // and move on to the next block immediately. + if (extensionHeaderLength == 0) { + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + switch (label) { + case GIF_GRAPHIC_CONTROL_LABEL: + // The GIF spec mandates that the Control Extension header block length is + // 4 bytes, and the parser for this block reads 4 bytes, so we must + // enforce that the buffer contains at least this many bytes. If the GIF + // specifies a different length, we allow that, so long as it's larger; + // the additional data will simply be ignored. + return Transition::To( + State::GRAPHIC_CONTROL_EXTENSION, + max<uint8_t>(extensionHeaderLength, GRAPHIC_CONTROL_EXTENSION_LEN)); + + case GIF_APPLICATION_EXTENSION_LABEL: + // Again, the spec specifies that an application extension header is 11 + // bytes, but for compatibility with GIFs in the wild, we allow deviation + // from the spec. This is important for real-world compatibility, as GIFs + // in the wild exist with application extension headers that are both + // shorter and longer than 11 bytes. However, we only try to actually + // interpret the application extension if the length is correct; + // otherwise, we just skip the block unconditionally. + return extensionHeaderLength == APPLICATION_EXTENSION_LEN + ? Transition::To(State::APPLICATION_IDENTIFIER, + extensionHeaderLength) + : Transition::ToUnbuffered( + State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + extensionHeaderLength); + + default: + // Skip over any other type of extension block, including comment and + // plain text blocks. + return Transition::ToUnbuffered(State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + extensionHeaderLength); + } +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadGraphicControlExtension(const char* aData) { + mGIFStruct.is_transparent = aData[0] & 0x1; + mGIFStruct.tpixel = uint8_t(aData[3]); + mGIFStruct.disposal_method = (aData[0] >> 2) & 0x7; + + if (mGIFStruct.disposal_method == 4) { + // Some encoders (and apparently some specs) represent + // DisposalMethod::RESTORE_PREVIOUS as 4, but 3 is used in the canonical + // spec and is more popular, so we normalize to 3. + mGIFStruct.disposal_method = 3; + } else if (mGIFStruct.disposal_method > 4) { + // This GIF is using a disposal method which is undefined in the spec. + // Treat it as DisposalMethod::NOT_SPECIFIED. + mGIFStruct.disposal_method = 0; + } + + DisposalMethod method = DisposalMethod(mGIFStruct.disposal_method); + if (method == DisposalMethod::CLEAR_ALL || method == DisposalMethod::CLEAR) { + // We may have to display the background under this image during animation + // playback, so we regard it as transparent. + PostHasTransparency(); + } + + mGIFStruct.delay_time = LittleEndian::readUint16(aData + 1) * 10; + if (!HasAnimation() && mGIFStruct.delay_time > 0) { + PostIsAnimated(FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time)); + } + + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadApplicationIdentifier( + const char* aData) { + if ((strncmp(aData, "NETSCAPE2.0", 11) == 0) || + (strncmp(aData, "ANIMEXTS1.0", 11) == 0)) { + // This is a Netscape application extension block. + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + } + + // This is an application extension we don't care about. Just skip it. + return Transition::To(State::SKIP_SUB_BLOCKS, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> +nsGIFDecoder2::ReadNetscapeExtensionSubBlock(const char* aData) { + const uint8_t blockLength = aData[0]; + if (blockLength == 0) { + // We hit the block terminator. + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + // We consume a minimum of 3 bytes in accordance with the specs for the + // Netscape application extension block, such as they are. + const size_t extensionLength = max<uint8_t>(blockLength, 3); + return Transition::To(State::NETSCAPE_EXTENSION_DATA, extensionLength); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadNetscapeExtensionData( + const char* aData) { + // Documentation for NETSCAPE2.0 / ANIMEXTS1.0 extensions can be found at: + // https://wiki.whatwg.org/wiki/GIF + static const uint8_t NETSCAPE_LOOPING_EXTENSION_SUB_BLOCK_ID = 1; + static const uint8_t NETSCAPE_BUFFERING_EXTENSION_SUB_BLOCK_ID = 2; + + const uint8_t subBlockID = aData[0] & 7; + switch (subBlockID) { + case NETSCAPE_LOOPING_EXTENSION_SUB_BLOCK_ID: + // This is looping extension. + mGIFStruct.loop_count = LittleEndian::readUint16(aData + 1); + // Zero loop count is infinite animation loop request. + if (mGIFStruct.loop_count == 0) { + mGIFStruct.loop_count = -1; + } + + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + + case NETSCAPE_BUFFERING_EXTENSION_SUB_BLOCK_ID: + // We allow, but ignore, this extension. + return Transition::To(State::NETSCAPE_EXTENSION_SUB_BLOCK, + SUB_BLOCK_HEADER_LEN); + + default: + return Transition::TerminateFailure(); + } +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadImageDescriptor( + const char* aData) { + // On the first frame, we don't need to yield, and none of the other checks + // below apply, so we can just jump right into FinishImageDescriptor(). + if (mGIFStruct.images_decoded == 0) { + return FinishImageDescriptor(aData); + } + + if (!HasAnimation()) { + // We should've already called PostIsAnimated(); this must be a corrupt + // animated image with a first frame timeout of zero. Signal that we're + // animated now, before the first-frame decode early exit below, so that + // RasterImage can detect that this happened. + PostIsAnimated(FrameTimeout::FromRawMilliseconds(0)); + } + + if (IsFirstFrameDecode()) { + // We're about to get a second frame, but we only want the first. Stop + // decoding now. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + MOZ_ASSERT(Size() == OutputSize(), "Downscaling an animated image?"); + + // Yield to allow access to the previous frame before we start a new one. + return Transition::ToAfterYield(State::FINISH_IMAGE_DESCRIPTOR); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::FinishImageDescriptor( + const char* aData) { + OrientedIntRect frameRect; + + // Get image offsets with respect to the screen origin. + frameRect.SetRect( + LittleEndian::readUint16(aData + 0), LittleEndian::readUint16(aData + 2), + LittleEndian::readUint16(aData + 4), LittleEndian::readUint16(aData + 6)); + + if (!mGIFStruct.images_decoded) { + // Work around GIF files where + // * at least one of the logical screen dimensions is smaller than the + // same dimension in the first image, or + // * GIF87a files where the first image's dimensions do not match the + // logical screen dimensions. + if (mGIFStruct.screen_height < frameRect.Height() || + mGIFStruct.screen_width < frameRect.Width() || + mGIFStruct.version == 87) { + mGIFStruct.screen_height = frameRect.Height(); + mGIFStruct.screen_width = frameRect.Width(); + frameRect.MoveTo(0, 0); + } + + // Create the image container with the right size. + BeginGIF(); + if (HasError()) { + // Setting the size led to an error. + return Transition::TerminateFailure(); + } + + // If we're doing a metadata decode, we're done. + if (IsMetadataDecode()) { + CheckForTransparency(frameRect); + FinishInternal(); + return Transition::TerminateSuccess(); + } + } + + // Work around broken GIF files that have zero frame width or height; in this + // case, we'll treat the frame as having the same size as the overall image. + if (frameRect.Height() == 0 || frameRect.Width() == 0) { + frameRect.SetHeight(mGIFStruct.screen_height); + frameRect.SetWidth(mGIFStruct.screen_width); + + // If that still resulted in zero frame width or height, give up. + if (frameRect.Height() == 0 || frameRect.Width() == 0) { + return Transition::TerminateFailure(); + } + } + + // Determine |depth| (log base 2 of the number of colors in the palette). + bool haveLocalColorTable = false; + uint16_t depth = 0; + uint8_t packedFields = aData[8]; + + if (packedFields & PACKED_FIELDS_COLOR_TABLE_BIT) { + // Get the palette depth from the local color table. + depth = (packedFields & PACKED_FIELDS_TABLE_DEPTH_MASK) + 1; + haveLocalColorTable = true; + } else { + // Get the palette depth from the global color table. + depth = mGIFStruct.global_colormap_depth; + } + + // If the transparent color index is greater than the number of colors in the + // color table, we may need a higher color depth than |depth| would specify. + // Our internal representation of the image will instead use |realDepth|, + // which is the smallest color depth that can accommodate the existing palette + // *and* the transparent color index. + uint16_t realDepth = depth; + while (mGIFStruct.tpixel >= (1 << realDepth) && realDepth < 8) { + realDepth++; + } + + // Create a mask used to ensure that color values fit within the colormap. + mColorMask = 0xFF >> (8 - realDepth); + + // Determine if this frame is interlaced or not. + const bool isInterlaced = packedFields & PACKED_FIELDS_INTERLACED_BIT; + + // Create the SurfacePipe we'll use to write output for this frame. + if (NS_FAILED(BeginImageFrame(frameRect, realDepth, isInterlaced))) { + return Transition::TerminateFailure(); + } + + // Clear state from last image. + mGIFStruct.pixels_remaining = + int64_t(frameRect.Width()) * int64_t(frameRect.Height()); + + if (haveLocalColorTable) { + // We have a local color table, so prepare to read it into the palette of + // the current frame. + mGIFStruct.local_colormap_size = 1 << depth; + + if (!mColormap) { + // Ensure our current colormap buffer is large enough to hold the new one. + mColormapSize = sizeof(uint32_t) << realDepth; + if (mGIFStruct.local_colormap_buffer_size < mColormapSize) { + if (mGIFStruct.local_colormap) { + free(mGIFStruct.local_colormap); + } + mGIFStruct.local_colormap_buffer_size = mColormapSize; + mGIFStruct.local_colormap = + static_cast<uint32_t*>(moz_xmalloc(mColormapSize)); + // Ensure the local colormap is initialized as opaque. + memset(mGIFStruct.local_colormap, 0xFF, mColormapSize); + } else { + mColormapSize = mGIFStruct.local_colormap_buffer_size; + } + + mColormap = mGIFStruct.local_colormap; + } + + MOZ_ASSERT(mColormap); + + const size_t size = 3 << depth; + if (mColormapSize > size) { + // Clear the part of the colormap which will be unused with this palette. + // If a GIF references an invalid palette entry, ensure the entry is + // opaque white. This is needed for Skia as if it isn't, RGBX surfaces + // will cause blending issues with Skia. + memset(reinterpret_cast<uint8_t*>(mColormap) + size, 0xFF, + mColormapSize - size); + } + + MOZ_ASSERT(mColorTablePos == 0); + + // We read the local color table in unbuffered mode since it can be quite + // large and it'd be preferable to avoid unnecessary copies. + return Transition::ToUnbuffered(State::FINISHED_LOCAL_COLOR_TABLE, + State::LOCAL_COLOR_TABLE, size); + } + + // There's no local color table; copy the global color table into the palette + // of the current frame. + if (mColormap) { + memcpy(mColormap, mGIFStruct.global_colormap, mColormapSize); + } else { + mColormap = mGIFStruct.global_colormap; + } + + return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadLocalColorTable( + const char* aData, size_t aLength) { + uint8_t* dest = reinterpret_cast<uint8_t*>(mColormap) + mColorTablePos; + memcpy(dest, aData, aLength); + mColorTablePos += aLength; + return Transition::ContinueUnbuffered(State::LOCAL_COLOR_TABLE); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::FinishedLocalColorTable() { + ConvertColormap(mColormap, mGIFStruct.local_colormap_size); + mColorTablePos = 0; + return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadImageDataBlock( + const char* aData) { + // Make sure the transparent pixel is transparent in the colormap. + if (mGIFStruct.is_transparent) { + // Save the old value so we can restore it later. + if (mColormap == mGIFStruct.global_colormap) { + mOldColor = mColormap[mGIFStruct.tpixel]; + } + mColormap[mGIFStruct.tpixel] = 0; + } + + // Initialize the LZW decoder. + mGIFStruct.datasize = uint8_t(aData[0]); + if (mGIFStruct.datasize > MAX_LZW_BITS) { + return Transition::TerminateFailure(); + } + const int clearCode = ClearCode(); + if (clearCode >= MAX_BITS) { + return Transition::TerminateFailure(); + } + + mGIFStruct.avail = clearCode + 2; + mGIFStruct.oldcode = -1; + mGIFStruct.codesize = mGIFStruct.datasize + 1; + mGIFStruct.codemask = (1 << mGIFStruct.codesize) - 1; + mGIFStruct.datum = mGIFStruct.bits = 0; + + // Initialize the tables. + for (int i = 0; i < clearCode; i++) { + mGIFStruct.suffix[i] = i; + } + + mGIFStruct.stackp = mGIFStruct.stack; + + // Begin reading image data sub-blocks. + return Transition::To(State::IMAGE_DATA_SUB_BLOCK, SUB_BLOCK_HEADER_LEN); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadImageDataSubBlock( + const char* aData) { + const uint8_t subBlockLength = aData[0]; + if (subBlockLength == 0) { + // We hit the block terminator. + EndImageFrame(); + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + if (mGIFStruct.pixels_remaining == 0) { + // We've already written to the entire image; we should've hit the block + // terminator at this point. This image is corrupt, but we'll tolerate it. + + if (subBlockLength == GIF_TRAILER) { + // This GIF is missing the block terminator for the final block; we'll put + // up with it. + FinishInternal(); + return Transition::TerminateSuccess(); + } + + // We're not at the end of the image, so just skip the extra data. + return Transition::ToUnbuffered(State::FINISHED_LZW_DATA, + State::SKIP_LZW_DATA, subBlockLength); + } + + // Handle the standard case: there's data in the sub-block and pixels left to + // fill in the image. We read the sub-block unbuffered so we can get pixels on + // the screen as soon as possible. + return Transition::ToUnbuffered(State::FINISHED_LZW_DATA, State::LZW_DATA, + subBlockLength); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::ReadLZWData( + const char* aData, size_t aLength) { + const uint8_t* data = reinterpret_cast<const uint8_t*>(aData); + size_t length = aLength; + + while (mGIFStruct.pixels_remaining > 0 && + (length > 0 || mGIFStruct.bits >= mGIFStruct.codesize)) { + size_t bytesRead = 0; + + auto result = mPipe.WritePixelBlocks<uint32_t>( + [&](uint32_t* aPixelBlock, int32_t aBlockSize) { + return YieldPixels<uint32_t>(data, length, &bytesRead, aPixelBlock, + aBlockSize); + }); + + if (MOZ_UNLIKELY(bytesRead > length)) { + MOZ_ASSERT_UNREACHABLE("Overread?"); + bytesRead = length; + } + + // Advance our position in the input based upon what YieldPixel() consumed. + data += bytesRead; + length -= bytesRead; + + switch (result) { + case WriteState::NEED_MORE_DATA: + continue; + + case WriteState::FINISHED: + NS_WARNING_ASSERTION(mGIFStruct.pixels_remaining <= 0, + "too many pixels"); + mGIFStruct.pixels_remaining = 0; + break; + + case WriteState::FAILURE: + return Transition::TerminateFailure(); + } + } + + // We're done, but keep going until we consume all the data in the sub-block. + return Transition::ContinueUnbuffered(State::LZW_DATA); +} + +LexerTransition<nsGIFDecoder2::State> nsGIFDecoder2::SkipSubBlocks( + const char* aData) { + // In the SKIP_SUB_BLOCKS state we skip over data sub-blocks that we're not + // interested in. Blocks consist of a block header (which can be up to 255 + // bytes in length) and a series of data sub-blocks. Each data sub-block + // consists of a single byte length value, followed by the data itself. A data + // sub-block with a length of zero terminates the overall block. + // SKIP_SUB_BLOCKS reads a sub-block length value. If it's zero, we've arrived + // at the next block. Otherwise, we enter the SKIP_DATA_THEN_SKIP_SUB_BLOCKS + // state to skip over the sub-block data and return to SKIP_SUB_BLOCKS at the + // start of the next sub-block. + + const uint8_t nextSubBlockLength = aData[0]; + if (nextSubBlockLength == 0) { + // We hit the block terminator, so the sequence of data sub-blocks is over; + // begin processing another block. + return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN); + } + + // Skip to the next sub-block length value. + return Transition::ToUnbuffered(State::FINISHED_SKIPPING_DATA, + State::SKIP_DATA_THEN_SKIP_SUB_BLOCKS, + nextSubBlockLength); +} + +Maybe<Telemetry::HistogramID> nsGIFDecoder2::SpeedHistogram() const { + return Some(Telemetry::IMAGE_DECODE_SPEED_GIF); +} + +} // namespace image +} // namespace mozilla |