summaryrefslogtreecommitdiffstats
path: root/image/decoders/nsGIFDecoder2.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /image/decoders/nsGIFDecoder2.cpp
parentInitial commit. (diff)
downloadfirefox-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.cpp1073
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