Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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:
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:
if (mGIFStruct.images_decoded > 0) {
return Transition::TerminateSuccess();
}
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