зеркало из https://github.com/mozilla/gecko-dev.git
1077 строки
38 KiB
C++
1077 строки
38 KiB
C++
/* -*- 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 "nsIInputStream.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)
|
|
, mGIFOpen(false)
|
|
, mSawTransparency(false)
|
|
{
|
|
// Clear out the structure, excluding the arrays.
|
|
memset(&mGIFStruct, 0, sizeof(mGIFStruct));
|
|
|
|
// Initialize as "animate once" in case no NETSCAPE2.0 extension is found.
|
|
mGIFStruct.loop_count = 1;
|
|
}
|
|
|
|
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 - 1);
|
|
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 IntRect& aFrameRect)
|
|
{
|
|
// Check if the image has a transparent color in its palette.
|
|
if (mGIFStruct.is_transparent) {
|
|
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.
|
|
IntRect 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 IntRect& aFrameRect,
|
|
uint16_t aDepth,
|
|
bool aIsInterlaced)
|
|
{
|
|
MOZ_ASSERT(HasSize());
|
|
|
|
bool hasTransparency = CheckForTransparency(aFrameRect);
|
|
gfx::SurfaceFormat format = hasTransparency ? SurfaceFormat::B8G8R8A8
|
|
: SurfaceFormat::B8G8R8X8;
|
|
|
|
// Make sure there's no animation if we're downscaling.
|
|
MOZ_ASSERT_IF(mDownscaler, !GetImageMetadata().HasAnimation());
|
|
|
|
SurfacePipeFlags pipeFlags = aIsInterlaced
|
|
? SurfacePipeFlags::DEINTERLACE
|
|
: SurfacePipeFlags();
|
|
|
|
Maybe<SurfacePipe> pipe;
|
|
if (mGIFStruct.images_decoded == 0) {
|
|
// The first frame may be displayed progressively.
|
|
pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY;
|
|
|
|
// The first frame is always decoded into an RGB surface.
|
|
pipe =
|
|
SurfacePipeFactory::CreateSurfacePipe(this, mGIFStruct.images_decoded,
|
|
GetSize(), OutputSize(),
|
|
aFrameRect, format, pipeFlags);
|
|
} else {
|
|
// This is an animation frame (and not the first). To minimize the memory
|
|
// usage of animations, the image data is stored in paletted form.
|
|
MOZ_ASSERT(!mDownscaler);
|
|
pipe =
|
|
SurfacePipeFactory::CreatePalettedSurfacePipe(this, mGIFStruct.images_decoded,
|
|
GetSize(), aFrameRect, format,
|
|
aDepth, pipeFlags);
|
|
}
|
|
|
|
mCurrentFrameIndex = mGIFStruct.images_decoded;
|
|
|
|
if (!pipe) {
|
|
mPipe = SurfacePipe();
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
mPipe = 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++;
|
|
|
|
// Tell the superclass we finished a frame
|
|
PostFrameStop(opacity,
|
|
DisposalMethod(mGIFStruct.disposal_method),
|
|
FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time));
|
|
|
|
// Reset the transparent pixel
|
|
if (mOldColor) {
|
|
mColormap[mGIFStruct.tpixel] = mOldColor;
|
|
mOldColor = 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>
|
|
NextPixel<PixelSize>
|
|
nsGIFDecoder2::YieldPixel(const uint8_t* aData,
|
|
size_t aLength,
|
|
size_t* aBytesReadOut)
|
|
{
|
|
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;
|
|
|
|
// 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 AsVariant(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 AsVariant(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 AsVariant(WriteState::FAILURE);
|
|
}
|
|
|
|
if (mGIFStruct.oldcode == -1) {
|
|
if (code >= MAX_BITS) {
|
|
return AsVariant(WriteState::FAILURE); // The code's too big; something's wrong.
|
|
}
|
|
|
|
mGIFStruct.firstchar = mGIFStruct.oldcode = code;
|
|
|
|
// Yield a pixel at the appropriate index in the colormap.
|
|
mGIFStruct.pixels_remaining--;
|
|
return AsVariant(ColormapIndexToPixel<PixelSize>(mGIFStruct.suffix[code]));
|
|
}
|
|
|
|
int incode = code;
|
|
if (code >= mGIFStruct.avail) {
|
|
*mGIFStruct.stackp++ = mGIFStruct.firstchar;
|
|
code = mGIFStruct.oldcode;
|
|
|
|
if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) {
|
|
return AsVariant(WriteState::FAILURE); // Stack overflow; something's wrong.
|
|
}
|
|
}
|
|
|
|
while (code >= clearCode) {
|
|
if ((code >= MAX_BITS) || (code == mGIFStruct.prefix[code])) {
|
|
return AsVariant(WriteState::FAILURE);
|
|
}
|
|
|
|
*mGIFStruct.stackp++ = mGIFStruct.suffix[code];
|
|
code = mGIFStruct.prefix[code];
|
|
|
|
if (mGIFStruct.stackp >= mGIFStruct.stack + MAX_BITS) {
|
|
return AsVariant(WriteState::FAILURE); // Stack overflow; something's wrong.
|
|
}
|
|
}
|
|
|
|
*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 AsVariant(WriteState::FAILURE);
|
|
}
|
|
|
|
// Yield a pixel at the appropriate index in the colormap.
|
|
mGIFStruct.pixels_remaining--;
|
|
return AsVariant(ColormapIndexToPixel<PixelSize>(*--mGIFStruct.stackp));
|
|
}
|
|
|
|
/// Expand the colormap from RGB to Packed ARGB as needed by Cairo.
|
|
/// And apply any LCMS transformation.
|
|
static void
|
|
ConvertColormap(uint32_t* aColormap, uint32_t aColors)
|
|
{
|
|
// Apply CMS transformation if enabled and available
|
|
if (gfxPlatform::GetCMSMode() == eCMSMode_All) {
|
|
qcms_transform* transform = gfxPlatform::GetCMSRGBTransform();
|
|
if (transform) {
|
|
qcms_transform_data(transform, aColormap, aColormap, aColors);
|
|
}
|
|
}
|
|
|
|
// Convert from the GIF's RGB format to the Cairo format.
|
|
// Work from end to begin, because of the in-place expansion
|
|
uint8_t* from = ((uint8_t*)aColormap) + 3 * aColors;
|
|
uint32_t* to = aColormap + aColors;
|
|
|
|
// Convert color entries to Cairo format
|
|
|
|
// set up for loops below
|
|
if (!aColors) {
|
|
return;
|
|
}
|
|
uint32_t c = aColors;
|
|
|
|
// copy as bytes until source pointer is 32-bit-aligned
|
|
// NB: can't use 32-bit reads, they might read off the end of the buffer
|
|
for (; (NS_PTR_TO_UINT32(from) & 0x3) && c; --c) {
|
|
from -= 3;
|
|
*--to = gfxPackedPixel(0xFF, from[0], from[1], from[2]);
|
|
}
|
|
|
|
// bulk copy of pixels.
|
|
while (c >= 4) {
|
|
from -= 12;
|
|
to -= 4;
|
|
c -= 4;
|
|
GFX_BLOCK_RGB_TO_FRGB(from,to);
|
|
}
|
|
|
|
// copy remaining pixel(s)
|
|
// NB: can't use 32-bit reads, they might read off the end of the buffer
|
|
while (c--) {
|
|
from -= 3;
|
|
*--to = gfxPackedPixel(0xFF, from[0], from[1], from[2]);
|
|
}
|
|
}
|
|
|
|
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::BLOCK_HEADER_AFTER_YIELD:
|
|
return Transition::To(State::BLOCK_HEADER, BLOCK_HEADER_LEN);
|
|
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::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 (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);
|
|
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)
|
|
{
|
|
if (mGIFStruct.images_decoded == 1) {
|
|
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();
|
|
}
|
|
|
|
if (mDownscaler) {
|
|
MOZ_ASSERT_UNREACHABLE("Doing downscale-during-decode for an animated "
|
|
"image?");
|
|
mDownscaler.reset();
|
|
}
|
|
}
|
|
|
|
IntRect frameRect;
|
|
|
|
// Get image offsets with respect to the screen origin.
|
|
frameRect.x = LittleEndian::readUint16(aData + 0);
|
|
frameRect.y = LittleEndian::readUint16(aData + 2);
|
|
frameRect.width = LittleEndian::readUint16(aData + 4);
|
|
frameRect.height = 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.height = mGIFStruct.screen_height;
|
|
frameRect.width = 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 accomodate 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 = frameRect.width * 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 (mGIFStruct.images_decoded == 0) {
|
|
// The first frame has a local color table. Allocate space for it as we
|
|
// use a BGRA or BGRX surface for the first frame; such surfaces don't
|
|
// have their own palettes internally.
|
|
mColormapSize = sizeof(uint32_t) << realDepth;
|
|
if (!mGIFStruct.local_colormap) {
|
|
mGIFStruct.local_colormap =
|
|
static_cast<uint32_t*>(moz_xmalloc(mColormapSize));
|
|
}
|
|
mColormap = mGIFStruct.local_colormap;
|
|
}
|
|
|
|
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 (mGIFStruct.images_decoded > 0) {
|
|
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]);
|
|
const int clearCode = ClearCode();
|
|
if (mGIFStruct.datasize > MAX_LZW_BITS || 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::ToAfterYield(State::BLOCK_HEADER_AFTER_YIELD);
|
|
}
|
|
|
|
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 (length > 0 && mGIFStruct.pixels_remaining > 0) {
|
|
size_t bytesRead = 0;
|
|
|
|
auto result = mGIFStruct.images_decoded == 0
|
|
? mPipe.WritePixels<uint32_t>([&]{ return YieldPixel<uint32_t>(data, length, &bytesRead); })
|
|
: mPipe.WritePixels<uint8_t>([&]{ return YieldPixel<uint8_t>(data, length, &bytesRead); });
|
|
|
|
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_WARN_IF(mGIFStruct.pixels_remaining > 0);
|
|
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);
|
|
}
|
|
|
|
Telemetry::ID
|
|
nsGIFDecoder2::SpeedHistogram()
|
|
{
|
|
return Telemetry::IMAGE_DECODE_SPEED_GIF;
|
|
}
|
|
|
|
} // namespace image
|
|
} // namespace mozilla
|