skia_use_fontconfig = is_linux
skia_use_freetype = is_android || is_fuchsia || is_linux
skia_use_gdi = false
- skia_use_giflib = !is_fuchsia
skia_use_libjpeg_turbo = true
skia_use_libpng = true
skia_use_libwebp = !is_fuchsia
"src/utils",
"src/utils/win",
"third_party/etc1",
+ "third_party/gif",
"third_party/ktx",
]
]
}
-optional("gif") {
- enabled = skia_use_giflib
- public_defines = [ "SK_HAS_GIF_LIBRARY" ]
-
- deps = [
- "//third_party/giflib",
- ]
- sources = [
- "src/codec/SkGifCodec.cpp",
- ]
-}
-
optional("gpu") {
enabled = skia_enable_gpu
public_defines = []
":fontmgr_custom",
":fontmgr_fontconfig",
":fontmgr_fuchsia",
- ":gif",
":gpu",
":hsw",
":jpeg",
"src/codec/SkBmpStandardCodec.cpp",
"src/codec/SkCodec.cpp",
"src/codec/SkCodecImageGenerator.cpp",
+ "src/codec/SkGifCodec.cpp",
"src/codec/SkMaskSwizzler.cpp",
"src/codec/SkMasks.cpp",
"src/codec/SkSampledCodec.cpp",
"src/codec/SkSampler.cpp",
+ "src/codec/SkStreamBuffer.cpp",
"src/codec/SkSwizzler.cpp",
"src/codec/SkWbmpCodec.cpp",
"src/images/SkImageEncoder.cpp",
"src/svg/SkSVGDevice.cpp",
"src/utils/mac/SkStream_mac.cpp",
"third_party/etc1/etc1.cpp",
+ "third_party/gif/GIFImageReader.cpp",
"third_party/ktx/ktx.cpp",
]
public_include_dirs = [ "gm" ]
sources = gm_sources
deps = [
+ ":flags",
":gpu_tool_utils",
":skia",
":tool_utils",
add_definitions(-DSK_HAS_GIF_LIBRARY)
else()
remove_srcs(../src/images/*GIF*)
- remove_srcs(../src/codec/*Gif*)
endif()
if (JPEG_TURBO_FOUND)
#include "sk_tool_utils.h"
#include "SkScan.h"
+#include <vector>
+
#ifdef SK_PDF_IMAGE_STATS
extern void SkPDFImageDumpStats();
#endif
static void push_codec_src(Path path, CodecSrc::Mode mode, CodecSrc::DstColorType dstColorType,
SkAlphaType dstAlphaType, float scale) {
if (FLAGS_simpleCodec) {
- if (mode != CodecSrc::kCodec_Mode || dstColorType != CodecSrc::kGetFromCanvas_DstColorType
- || scale != 1.0f)
+ const bool simple = CodecSrc::kCodec_Mode == mode || CodecSrc::kAnimated_Mode == mode;
+ if (!simple || dstColorType != CodecSrc::kGetFromCanvas_DstColorType || scale != 1.0f) {
// Only decode in the simple case.
return;
+ }
}
SkString folder;
switch (mode) {
case CodecSrc::kSubset_Mode:
folder.append("codec_subset");
break;
+ case CodecSrc::kAnimated_Mode:
+ folder.append("codec_animated");
+ break;
}
switch (dstColorType) {
}
}
+ {
+ std::vector<SkCodec::FrameInfo> frameInfos = codec->getFrameInfo();
+ if (frameInfos.size() > 1) {
+ push_codec_src(path, CodecSrc::kAnimated_Mode, CodecSrc::kGetFromCanvas_DstColorType,
+ kPremul_SkAlphaType, 1.0f);
+ }
+
+ }
+
if (FLAGS_simpleCodec) {
return;
}
const int bpp = SkColorTypeBytesPerPixel(decodeInfo.colorType());
const size_t rowBytes = size.width() * bpp;
- SkAutoMalloc pixels(decodeInfo.getSafeSize(rowBytes));
+ const size_t safeSize = decodeInfo.getSafeSize(rowBytes);
+ SkAutoMalloc pixels(safeSize);
SkPMColor colorPtr[256];
int colorCount = 256;
}
switch (fMode) {
+ case kAnimated_Mode: {
+ std::vector<SkCodec::FrameInfo> frameInfos = codec->getFrameInfo();
+ if (frameInfos.size() <= 1) {
+ return SkStringPrintf("%s is not an animated image.", fPath.c_str());
+ }
+
+ SkAutoCanvasRestore acr(canvas, true);
+ // Used to cache a frame that future frames will depend on.
+ SkAutoMalloc priorFramePixels;
+ size_t cachedFrame = SkCodec::kNone;
+ for (size_t i = 0; i < frameInfos.size(); i++) {
+ options.fFrameIndex = i;
+ // Check for a prior frame
+ const size_t reqFrame = frameInfos[i].fRequiredFrame;
+ if (reqFrame != SkCodec::kNone && reqFrame == cachedFrame
+ && priorFramePixels.get()) {
+ // Copy into pixels
+ memcpy(pixels.get(), priorFramePixels.get(), safeSize);
+ options.fHasPriorFrame = true;
+ } else {
+ options.fHasPriorFrame = false;
+ }
+ const SkCodec::Result result = codec->getPixels(decodeInfo, pixels.get(),
+ rowBytes, &options,
+ colorPtr, &colorCount);
+ switch (result) {
+ case SkCodec::kSuccess:
+ case SkCodec::kIncompleteInput:
+ draw_to_canvas(canvas, bitmapInfo, pixels.get(), rowBytes,
+ colorPtr, colorCount, fDstColorType);
+ if (result == SkCodec::kIncompleteInput) {
+ return "";
+ }
+ break;
+ default:
+ return SkStringPrintf("Couldn't getPixels for frame %i in %s.",
+ i, fPath.c_str());
+ }
+
+ // If a future frame depends on this one, store it in priorFrame.
+ // (Note that if i+1 does *not* depend on i, then no future frame can.)
+ if (i+1 < frameInfos.size() && frameInfos[i+1].fRequiredFrame == i) {
+ memcpy(priorFramePixels.reset(safeSize), pixels.get(), safeSize);
+ cachedFrame = i;
+ }
+
+ canvas->translate(SkIntToScalar(0), SkIntToScalar(decodeInfo.height()));
+ }
+ break;
+ }
case kCodecZeroInit_Mode:
case kCodec_Mode: {
switch (codec->getPixels(decodeInfo, pixels.get(), rowBytes, &options,
case kScanline_Mode: {
void* dst = pixels.get();
uint32_t height = decodeInfo.height();
- const bool png = fPath.endsWith("png");
+ const bool useIncremental = [this]() {
+ auto exts = { "png", "PNG", "gif", "GIF" };
+ for (auto ext : exts) {
+ if (fPath.endsWith(ext)) {
+ return true;
+ }
+ }
+ return false;
+ }();
+ // ico may use the old scanline method or the new one, depending on whether it
+ // internally holds a bmp or a png.
const bool ico = fPath.endsWith("ico");
- bool useOldScanlineMethod = !png && !ico;
- if (png || ico) {
+ bool useOldScanlineMethod = !useIncremental && !ico;
+ if (useIncremental || ico) {
if (SkCodec::kSuccess == codec->startIncrementalDecode(decodeInfo, dst,
rowBytes, nullptr, colorPtr, &colorCount)) {
int rowsDecoded;
rowsDecoded);
}
} else {
- if (png) {
- // Error: PNG should support incremental decode.
+ if (useIncremental) {
+ // Error: These should support incremental decode.
return "Could not start incremental decode";
}
// Otherwise, this is an ICO. Since incremental failed, it must contain a BMP,
// image, memory will be filled with a default value.
codec->getScanlines(dst, height, rowBytes);
break;
- case SkCodec::kOutOfOrder_SkScanlineOrder: {
- for (int y = 0; y < decodeInfo.height(); y++) {
- int dstY = codec->outputScanline(y);
- void* dstPtr = SkTAddOffset<void>(dst, rowBytes * dstY);
- // We complete the loop, even if this call begins to fail
- // due to an incomplete image. This ensures any uninitialized
- // memory will be filled with the proper value.
- codec->getScanlines(dstPtr, 1, rowBytes);
- }
- break;
- }
}
}
if (nullptr == codec) {
return SkISize::Make(0, 0);
}
- return codec->getScaledDimensions(fScale);
+
+ auto imageSize = codec->getScaledDimensions(fScale);
+ if (fMode == kAnimated_Mode) {
+ // We'll draw one of each frame, so make it big enough to hold them all.
+ const size_t count = codec->getFrameInfo().size();
+ imageSize.fHeight = imageSize.fHeight * count;
+ }
+ return imageSize;
}
Name CodecSrc::name() const {
kStripe_Mode, // Tests the skipping of scanlines
kCroppedScanline_Mode, // Tests (jpeg) cropped scanline optimization
kSubset_Mode, // For codecs that support subsets directly.
+ kAnimated_Mode, // For codecs that support animation.
};
enum DstColorType {
kGetFromCanvas_DstColorType,
// image, memory will be filled with a default value.
codec->getScanlines(dst, height, rowBytes);
break;
- case SkCodec::kOutOfOrder_SkScanlineOrder: {
- for (int y = 0; y < decodeInfo.height(); y++) {
- int dstY = codec->outputScanline(y);
- void* dstPtr = bitmap.getAddr(0, dstY);
- // We complete the loop, even if this call begins to fail
- // due to an incomplete image. This ensures any uninitialized
- // memory will be filled with the proper value.
- codec->getScanlines(dstPtr, 1, bitmap.rowBytes());
- }
- break;
- }
}
SkDebugf("[terminated] Success!\n");
break;
--- /dev/null
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm.h"
+#include "SkAnimTimer.h"
+#include "SkCanvas.h"
+#include "SkCodec.h"
+#include "SkColor.h"
+#include "SkCommandLineFlags.h"
+#include "SkPaint.h"
+#include "SkString.h"
+#include "Resources.h"
+
+#include <vector>
+
+DEFINE_string(animatedGif, "test640x479.gif", "Animated gif in resources folder");
+
+namespace {
+ void error(SkCanvas* canvas, const SkString& errorText) {
+ constexpr SkScalar kOffset = 5.0f;
+ canvas->drawColor(SK_ColorRED);
+ SkPaint paint;
+ SkRect bounds;
+ paint.measureText(errorText.c_str(), errorText.size(), &bounds);
+ canvas->drawText(errorText.c_str(), errorText.size(), kOffset, bounds.height() + kOffset,
+ paint);
+ }
+}
+
+class AnimatedGifGM : public skiagm::GM {
+private:
+ std::unique_ptr<SkCodec> fCodec;
+ size_t fFrame;
+ double fNextUpdate;
+ size_t fTotalFrames;
+ std::vector<SkCodec::FrameInfo> fFrameInfos;
+ std::vector<SkBitmap> fFrames;
+
+ void drawFrame(SkCanvas* canvas, int frameIndex) {
+ // FIXME: Create from an Image/ImageGenerator?
+ if (frameIndex >= (int) fFrames.size()) {
+ fFrames.resize(frameIndex + 1);
+ }
+ SkBitmap& bm = fFrames[frameIndex];
+ if (!bm.getPixels()) {
+ const SkImageInfo info = fCodec->getInfo().makeColorType(kN32_SkColorType);
+ bm.allocPixels(info);
+
+ SkCodec::Options opts;
+ opts.fFrameIndex = frameIndex;
+ opts.fHasPriorFrame = false;
+ const size_t requiredFrame = fFrameInfos[frameIndex].fRequiredFrame;
+ if (requiredFrame != SkCodec::kNone) {
+ SkASSERT(requiredFrame < fFrames.size());
+ SkBitmap& requiredBitmap = fFrames[requiredFrame];
+ // For simplicity, do not try to cache old frames
+ if (requiredBitmap.getPixels() && requiredBitmap.copyTo(&bm)) {
+ opts.fHasPriorFrame = true;
+ }
+ }
+
+ if (SkCodec::kSuccess != fCodec->getPixels(info, bm.getPixels(),
+ bm.rowBytes(), &opts,
+ nullptr, nullptr)) {
+ SkDebugf("Could not getPixels for frame %i: %s", frameIndex, FLAGS_animatedGif[0]);
+ return;
+ }
+ }
+
+ canvas->drawBitmap(bm, 0, 0);
+ }
+
+public:
+ AnimatedGifGM()
+ : fFrame(0)
+ , fNextUpdate (-1)
+ , fTotalFrames (-1) {}
+
+private:
+ SkString onShortName() override {
+ return SkString("animatedGif");
+ }
+
+ SkISize onISize() override {
+ if (this->initCodec()) {
+ SkISize dim = fCodec->getInfo().dimensions();
+ // Wide enough to display all the frames.
+ dim.fWidth *= fTotalFrames;
+ // Tall enough to show the row of frames plus an animating version.
+ dim.fHeight *= 2;
+ return dim;
+ }
+ return SkISize::Make(640, 480);
+ }
+
+ void onDrawBackground(SkCanvas* canvas) override {
+ if (this->initCodec()) {
+ SkAutoCanvasRestore acr(canvas, true);
+ for (size_t frameIndex = 0; frameIndex < fTotalFrames; frameIndex++) {
+ this->drawFrame(canvas, frameIndex);
+ canvas->translate(fCodec->getInfo().width(), 0);
+ }
+ }
+ }
+
+ bool initCodec() {
+ if (fCodec) {
+ return true;
+ }
+ if (FLAGS_animatedGif.isEmpty()) {
+ SkDebugf("Nothing specified for --animatedGif!");
+ return false;
+ }
+
+ std::unique_ptr<SkStream> stream(GetResourceAsStream(FLAGS_animatedGif[0]));
+ if (!stream) {
+ return false;
+ }
+
+ fCodec.reset(SkCodec::NewFromStream(stream.release()));
+ if (!fCodec) {
+ SkDebugf("Could create codec from %s", FLAGS_animatedGif[0]);
+ return false;
+ }
+
+ fFrame = 0;
+ fFrameInfos = fCodec->getFrameInfo();
+ fTotalFrames = fFrameInfos.size();
+ return true;
+ }
+
+ void onDraw(SkCanvas* canvas) override {
+ if (!fCodec) {
+ SkString errorText = SkStringPrintf("Nothing to draw; %s", FLAGS_animatedGif[0]);
+ error(canvas, errorText);
+ return;
+ }
+
+ SkAutoCanvasRestore acr(canvas, true);
+ canvas->translate(0, fCodec->getInfo().height());
+ this->drawFrame(canvas, fFrame);
+ }
+
+ bool onAnimate(const SkAnimTimer& timer) override {
+ if (!fCodec || fTotalFrames == 1) {
+ return false;
+ }
+
+ double secs = timer.msec() * .1;
+ if (fNextUpdate < double(0)) {
+ // This is a sentinel that we have not done any updates yet.
+ // I'm assuming this gets called *after* onOnceBeforeDraw, so our first frame should
+ // already have been retrieved.
+ SkASSERT(fFrame == 0);
+ fNextUpdate = secs + fFrameInfos[fFrame].fDuration;
+
+ return true;
+ }
+
+ if (secs < fNextUpdate) {
+ return true;
+ }
+
+ while (secs >= fNextUpdate) {
+ // Retrieve the next frame.
+ fFrame++;
+ if (fFrame == fTotalFrames) {
+ fFrame = 0;
+ }
+
+ // Note that we loop here. This is not safe if we need to draw the intermediate frame
+ // in order to draw correctly.
+ fNextUpdate += fFrameInfos[fFrame].fDuration;
+ }
+
+ return true;
+ }
+};
+
+DEF_GM(return new AnimatedGifGM);
+
'standalone_static_library': 1,
'dependencies': [
'core.gyp:*',
- 'giflib.gyp:giflib',
'libjpeg-turbo-selector.gyp:libjpeg-turbo-selector',
'libpng.gyp:libpng',
'libwebp.gyp:libwebp',
'../src/codec',
'../src/core',
'../src/utils',
+ '../third_party/gif',
],
'sources': [
'../src/codec/SkAndroidCodec.cpp',
'../src/codec/SkPngCodec.cpp',
'../src/codec/SkSampler.cpp',
'../src/codec/SkSampledCodec.cpp',
+ '../src/codec/SkStreamBuffer.cpp',
'../src/codec/SkSwizzler.cpp',
'../src/codec/SkWbmpCodec.cpp',
'../src/codec/SkWebpAdapterCodec.cpp',
'../src/codec/SkCodecImageGenerator.cpp',
'../src/ports/SkImageGenerator_skia.cpp',
+
+ '../third_party/gif/GIFImageReader.cpp',
],
'direct_dependent_settings': {
'include_dirs': [
#include "SkTypes.h"
#include "SkYUVSizeInfo.h"
+#include <vector>
+
class SkColorSpace;
class SkColorSpaceXform;
class SkData;
struct Options {
Options()
: fZeroInitialized(kNo_ZeroInitialized)
- , fSubset(NULL)
+ , fSubset(nullptr)
+ , fFrameIndex(0)
+ , fHasPriorFrame(false)
{}
- ZeroInitialized fZeroInitialized;
+ ZeroInitialized fZeroInitialized;
/**
* If not NULL, represents a subset of the original image to decode.
* Must be within the bounds returned by getInfo().
* subset left and subset width to decode partial scanlines on calls
* to getScanlines().
*/
- SkIRect* fSubset;
+ const SkIRect* fSubset;
+
+ /**
+ * The frame to decode.
+ *
+ * Only meaningful for multi-frame images.
+ */
+ size_t fFrameIndex;
+
+ /**
+ * If true, the dst already contains the prior frame.
+ *
+ * Only meaningful for multi-frame images.
+ *
+ * If fFrameIndex needs to be blended with a prior frame (as reported by
+ * getFrameInfo[fFrameIndex].fRequiredFrame), the client can set this to
+ * either true or false:
+ *
+ * true means that the prior frame is already in the dst, and this
+ * codec only needs to decode fFrameIndex and blend it with the dst.
+ * Options.fZeroInitialized is ignored in this case.
+ *
+ * false means that the dst does not contain the prior frame, so this
+ * codec needs to first decode the prior frame (which in turn may need
+ * to decode its prior frame).
+ */
+ bool fHasPriorFrame;
};
/**
* Upside down bmps are an example.
*/
kBottomUp_SkScanlineOrder,
-
- /*
- * This indicates that the scanline decoder reliably outputs rows, but
- * they will not be in logical order. If the scanline format is
- * kOutOfOrder, the nextScanline() API should be used to determine the
- * actual y-coordinate of the next output row.
- *
- * For this scanline ordering, it is advisable to get and skip
- * scanlines one at a time.
- *
- * Interlaced gifs are an example.
- */
- kOutOfOrder_SkScanlineOrder,
};
/**
* decoder.
*
* This will equal fCurrScanline, except in the case of strangely
- * encoded image types (bottom-up bmps, interlaced gifs).
+ * encoded image types (bottom-up bmps).
*
* Results are undefined when not in scanline decoding mode.
*/
*/
int outputScanline(int inputScanline) const;
+ // The required frame for an independent frame is marked as
+ // kNone.
+ static constexpr size_t kNone = static_cast<size_t>(-1);
+
+ /**
+ * Information about individual frames in a multi-framed image.
+ */
+ struct FrameInfo {
+ /**
+ * The frame that this frame needs to be blended with, or
+ * kNone.
+ */
+ size_t fRequiredFrame;
+
+ /**
+ * Number of milliseconds to show this frame.
+ */
+ size_t fDuration;
+ };
+
+ /**
+ * Return info about the frames in the image.
+ *
+ * May require reading through the stream to determine the number of
+ * frames.
+ *
+ * As such, future decoding calls may require a rewind.
+ *
+ * For single-frame images, this will return an empty vector.
+ */
+ std::vector<FrameInfo> getFrameInfo() {
+ return this->onGetFrameInfo();
+ }
+
protected:
/**
* Takes ownership of SkStream*
bool initializeColorXform(const SkImageInfo& dstInfo);
SkColorSpaceXform* colorXform() const { return fColorXform.get(); }
+ virtual std::vector<FrameInfo> onGetFrameInfo() {
+ // empty vector - this is not animated.
+ return {};
+ }
+
/**
* Used for testing with qcms.
* FIXME: Remove this when we are done comparing with qcms.
* May create a sampler, if one is not currently being used. Otherwise, does
* not affect ownership.
*
- * Only valid during scanline decoding.
+ * Only valid during scanline decoding or incremental decoding.
*/
virtual SkSampler* getSampler(bool /*createIfNecessary*/) { return nullptr; }
#ifdef SK_HAS_JPEG_LIBRARY
case kJPEG_SkEncodedFormat:
#endif
-#ifdef SK_HAS_GIF_LIBRARY
case kGIF_SkEncodedFormat:
-#endif
case kBMP_SkEncodedFormat:
case kWBMP_SkEncodedFormat:
return new SkSampledCodec(codec.release());
#ifdef SK_HAS_WEBP_LIBRARY
{ SkWebpCodec::IsWebp, SkWebpCodec::NewFromStream },
#endif
-#ifdef SK_HAS_GIF_LIBRARY
{ SkGifCodec::IsGif, SkGifCodec::NewFromStream },
-#endif
#ifdef SK_HAS_PNG_LIBRARY
{ SkIcoCodec::IsIco, SkIcoCodec::NewFromStream },
#endif
SkCodec::~SkCodec() {}
bool SkCodec::rewindIfNeeded() {
- if (!fStream) {
- // Some codecs do not have a stream. They may hold onto their own data or another codec.
- // They must handle rewinding themselves.
- return true;
- }
-
// Store the value of fNeedsRewind so we can update it. Next read will
// require a rewind.
const bool needsRewind = fNeedsRewind;
// startIncrementalDecode will need to be called before incrementalDecode.
fStartedIncrementalDecode = false;
- if (!fStream->rewind()) {
+ // Some codecs do not have a stream. They may hold onto their own data or another codec.
+ // They must handle rewinding themselves.
+ if (fStream && !fStream->rewind()) {
return false;
}
fill_proc(fillInfo, fillDst, rowBytes, fillValue, zeroInit, sampler);
break;
}
- case kOutOfOrder_SkScanlineOrder: {
- SkASSERT(1 == linesRequested || this->getInfo().height() == linesRequested);
- const SkImageInfo fillInfo = info.makeWH(fillWidth, 1);
- for (int srcY = linesDecoded; srcY < linesRequested; srcY++) {
- fillDst = SkTAddOffset<void>(dst, this->outputScanline(srcY) * rowBytes);
- fill_proc(fillInfo, fillDst, rowBytes, fillValue, zeroInit, sampler);
- }
- break;
- }
}
}
--- /dev/null
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/*
+ * Copyright (C) 2015 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SkCodecAnimation_DEFINED
+#define SkCodecAnimation_DEFINED
+
+#include "SkCodec.h"
+#include "SkRect.h"
+
+class SkCodecAnimation {
+public:
+
+ // GIF and WebP support animation. The explanation below is in terms of GIF,
+ // but the same constants are used for WebP, too.
+ // GIFs have an optional 16-bit unsigned loop count that describes how an
+ // animated GIF should be cycled. If the loop count is absent, the animation
+ // cycles once; if it is 0, the animation cycles infinitely; otherwise the
+ // animation plays n + 1 cycles (where n is the specified loop count). If the
+ // GIF decoder defaults to kAnimationLoopOnce in the absence of any loop count
+ // and translates an explicit "0" loop count to kAnimationLoopInfinite, then we
+ // get a couple of nice side effects:
+ // * By making kAnimationLoopOnce be 0, we allow the animation cycling code to
+ // avoid special-casing it, and simply treat all non-negative loop counts
+ // identically.
+ // * By making the other two constants negative, we avoid conflicts with any
+ // real loop count values.
+ static const int kAnimationLoopOnce = 0;
+ static const int kAnimationLoopInfinite = -1;
+ static const int kAnimationNone = -2;
+
+ /**
+ * This specifies how the next frame is based on this frame.
+ *
+ * Names are based on the GIF 89a spec.
+ *
+ * The numbers correspond to values in a GIF.
+ */
+ enum DisposalMethod {
+ /**
+ * The next frame should be drawn on top of this one.
+ *
+ * In a GIF, a value of 0 (not specified) is also treated as Keep.
+ */
+ Keep_DisposalMethod = 1,
+
+ /**
+ * Similar to Keep, except the area inside this frame's rectangle
+ * should be cleared to the BackGround color (transparent) before
+ * drawing the next frame.
+ */
+ RestoreBGColor_DisposalMethod = 2,
+
+ /**
+ * The next frame should be drawn on top of the previous frame - i.e.
+ * disregarding this one.
+ *
+ * In a GIF, a value of 4 is also treated as RestorePrevious.
+ */
+ RestorePrevious_DisposalMethod = 3,
+ };
+
+private:
+ SkCodecAnimation();
+};
+#endif // SkCodecAnimation_DEFINED
* found in the LICENSE file.
*/
+/*
+ * Copyright (C) 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SkCodecAnimation.h"
#include "SkCodecPriv.h"
#include "SkColorPriv.h"
#include "SkColorTable.h"
#include "SkGifCodec.h"
#include "SkStream.h"
#include "SkSwizzler.h"
-#include "SkUtils.h"
-#include "gif_lib.h"
+#include <algorithm>
+
+#define GIF87_STAMP "GIF87a"
+#define GIF89_STAMP "GIF89a"
+#define GIF_STAMP_LEN 6
/*
* Checks the start of the stream to see if the image is a gif
*/
bool SkGifCodec::IsGif(const void* buf, size_t bytesRead) {
if (bytesRead >= GIF_STAMP_LEN) {
- if (memcmp(GIF_STAMP, buf, GIF_STAMP_LEN) == 0 ||
- memcmp(GIF87_STAMP, buf, GIF_STAMP_LEN) == 0 ||
+ if (memcmp(GIF87_STAMP, buf, GIF_STAMP_LEN) == 0 ||
memcmp(GIF89_STAMP, buf, GIF_STAMP_LEN) == 0)
{
return true;
return result;
}
-
-/*
- * Read function that will be passed to gif_lib
- */
-static int32_t read_bytes_callback(GifFileType* fileType, GifByteType* out, int32_t size) {
- SkStream* stream = (SkStream*) fileType->UserData;
- return (int32_t) stream->read(out, size);
-}
-
/*
- * Open the gif file
- */
-static GifFileType* open_gif(SkStream* stream) {
-#if GIFLIB_MAJOR < 5
- return DGifOpen(stream, read_bytes_callback);
-#else
- return DGifOpen(stream, read_bytes_callback, nullptr);
-#endif
-}
-
-/*
- * Check if a there is an index of the color table for a transparent pixel
+ * Assumes IsGif was called and returned true
+ * Creates a gif decoder
+ * Reads enough of the stream to determine the image format
*/
-static uint32_t find_trans_index(const SavedImage& image) {
- // If there is a transparent index specified, it will be contained in an
- // extension block. We will loop through extension blocks in reverse order
- // to check the most recent extension blocks first.
- for (int32_t i = image.ExtensionBlockCount - 1; i >= 0; i--) {
- // Get an extension block
- const ExtensionBlock& extBlock = image.ExtensionBlocks[i];
-
- // Specifically, we need to check for a graphics control extension,
- // which may contain transparency information. Also, note that a valid
- // graphics control extension is always four bytes. The fourth byte
- // is the transparent index (if it exists), so we need at least four
- // bytes.
- if (GRAPHICS_EXT_FUNC_CODE == extBlock.Function && extBlock.ByteCount >= 4) {
- // Check the transparent color flag which indicates whether a
- // transparent index exists. It is the least significant bit of
- // the first byte of the extension block.
- if (1 == (extBlock.Bytes[0] & 1)) {
- // Use uint32_t to prevent sign extending
- return extBlock.Bytes[3];
- }
-
- // There should only be one graphics control extension for the image frame
- break;
- }
+SkCodec* SkGifCodec::NewFromStream(SkStream* stream) {
+ std::unique_ptr<GIFImageReader> reader(new GIFImageReader(stream));
+ if (!reader->parse(GIFImageReader::GIFSizeQuery)) {
+ // Not enough data to determine the size.
+ return nullptr;
}
- // Use maximum unsigned int (surely an invalid index) to indicate that a valid
- // index was not found.
- return SK_MaxU32;
-}
-
-inline uint32_t ceil_div(uint32_t a, uint32_t b) {
- return (a + b - 1) / b;
-}
-
-/*
- * Gets the output row corresponding to the encoded row for interlaced gifs
- */
-inline uint32_t get_output_row_interlaced(uint32_t encodedRow, uint32_t height) {
- SkASSERT(encodedRow < height);
- // First pass
- if (encodedRow * 8 < height) {
- return encodedRow * 8;
- }
- // Second pass
- if (encodedRow * 4 < height) {
- return 4 + 8 * (encodedRow - ceil_div(height, 8));
+ if (0 == reader->screenWidth() || 0 == reader->screenHeight()) {
+ return nullptr;
}
- // Third pass
- if (encodedRow * 2 < height) {
- return 2 + 4 * (encodedRow - ceil_div(height, 4));
- }
- // Fourth pass
- return 1 + 2 * (encodedRow - ceil_div(height, 2));
-}
-/*
- * This function cleans up the gif object after the decode completes
- * It is used in a SkAutoTCallIProc template
- */
-void SkGifCodec::CloseGif(GifFileType* gif) {
-#if GIFLIB_MAJOR < 5 || (GIFLIB_MAJOR == 5 && GIFLIB_MINOR == 0)
- DGifCloseFile(gif);
-#else
- DGifCloseFile(gif, nullptr);
-#endif
+ const auto alpha = reader->firstFrameHasAlpha() ? SkEncodedInfo::kBinary_Alpha
+ : SkEncodedInfo::kOpaque_Alpha;
+ // Use kPalette since Gifs are encoded with a color table.
+ // FIXME: Gifs can actually be encoded with 4-bits per pixel. Using 8 works, but we could skip
+ // expanding to 8 bits and take advantage of the SkSwizzler to work from 4.
+ const auto encodedInfo = SkEncodedInfo::Make(SkEncodedInfo::kPalette_Color, alpha, 8);
+
+ // Although the encodedInfo is always kPalette_Color, it is possible that kIndex_8 is
+ // unsupported if the frame is subset and there is no transparent pixel.
+ const auto colorType = reader->firstFrameSupportsIndex8() ? kIndex_8_SkColorType
+ : kN32_SkColorType;
+ // The choice of unpremul versus premul is arbitrary, since all colors are either fully
+ // opaque or fully transparent (i.e. kBinary), but we stored the transparent colors as all
+ // zeroes, which is arguably premultiplied.
+ const auto alphaType = reader->firstFrameHasAlpha() ? kUnpremul_SkAlphaType
+ : kOpaque_SkAlphaType;
+ // FIXME: GIF should default to SkColorSpace::NewNamed(SkColorSpace::kSRGB_Named).
+ const auto imageInfo = SkImageInfo::Make(reader->screenWidth(), reader->screenHeight(),
+ colorType, alphaType);
+ return new SkGifCodec(encodedInfo, imageInfo, reader.release());
}
-/*
- * This function free extension data that has been saved to assist the image
- * decoder
- */
-void SkGifCodec::FreeExtension(SavedImage* image) {
- if (NULL != image->ExtensionBlocks) {
-#if GIFLIB_MAJOR < 5
- FreeExtension(image);
-#else
- GifFreeExtensions(&image->ExtensionBlockCount, &image->ExtensionBlocks);
-#endif
- }
+bool SkGifCodec::onRewind() {
+ fReader->clearDecodeState();
+ return true;
}
-/*
- * Read enough of the stream to initialize the SkGifCodec.
- * Returns a bool representing success or failure.
- *
- * @param codecOut
- * If it returned true, and codecOut was not nullptr,
- * codecOut will be set to a new SkGifCodec.
- *
- * @param gifOut
- * If it returned true, and codecOut was nullptr,
- * gifOut must be non-nullptr and gifOut will be set to a new
- * GifFileType pointer.
- *
- * @param stream
- * Deleted on failure.
- * codecOut will take ownership of it in the case where we created a codec.
- * Ownership is unchanged when we returned a gifOut.
- *
- */
-bool SkGifCodec::ReadHeader(SkStream* stream, SkCodec** codecOut, GifFileType** gifOut) {
- SkAutoTDelete<SkStream> streamDeleter(stream);
-
- // Read gif header, logical screen descriptor, and global color table
- SkAutoTCallVProc<GifFileType, CloseGif> gif(open_gif(stream));
-
- if (nullptr == gif) {
- gif_error("DGifOpen failed.\n");
- return false;
- }
-
- // Read through gif extensions to get to the image data. Set the
- // transparent index based on the extension data.
- uint32_t transIndex;
- SkCodec::Result result = ReadUpToFirstImage(gif, &transIndex);
- if (kSuccess != result){
- return false;
- }
-
- // Read the image descriptor
- if (GIF_ERROR == DGifGetImageDesc(gif)) {
- return false;
- }
- // If reading the image descriptor is successful, the image count will be
- // incremented.
- SkASSERT(gif->ImageCount >= 1);
-
- if (nullptr != codecOut) {
- SkISize size;
- SkIRect frameRect;
- if (!GetDimensions(gif, &size, &frameRect)) {
- gif_error("Invalid gif size.\n");
- return false;
- }
- bool frameIsSubset = (size != frameRect.size());
-
- // Determine the encoded alpha type. The transIndex might be valid if it less
- // than 256. We are not certain that the index is valid until we process the color
- // table, since some gifs have color tables with less than 256 colors. If
- // there might be a valid transparent index, we must indicate that the image has
- // alpha.
- // In the case where we must support alpha, we indicate kBinary, since every
- // pixel will either be fully opaque or fully transparent.
- SkEncodedInfo::Alpha alpha = (transIndex < 256) ? SkEncodedInfo::kBinary_Alpha :
- SkEncodedInfo::kOpaque_Alpha;
-
- // Return the codec
- // Use kPalette since Gifs are encoded with a color table.
- // Use 8-bits per component, since this is the output we get from giflib.
- // FIXME: Gifs can actually be encoded with 4-bits per pixel. Can we support this?
- SkEncodedInfo info = SkEncodedInfo::Make(SkEncodedInfo::kPalette_Color, alpha, 8);
- *codecOut = new SkGifCodec(size.width(), size.height(), info, streamDeleter.release(),
- gif.release(), transIndex, frameRect, frameIsSubset);
- } else {
- SkASSERT(nullptr != gifOut);
- streamDeleter.release();
- *gifOut = gif.release();
- }
- return true;
+SkGifCodec::SkGifCodec(const SkEncodedInfo& encodedInfo, const SkImageInfo& imageInfo,
+ GIFImageReader* reader)
+ : INHERITED(encodedInfo, imageInfo, nullptr)
+ , fReader(reader)
+ , fTmpBuffer(nullptr)
+ , fSwizzler(nullptr)
+ , fCurrColorTable(nullptr)
+ , fCurrColorTableIsReal(false)
+ , fFilledBackground(false)
+ , fFirstCallToIncrementalDecode(false)
+ , fDst(nullptr)
+ , fDstRowBytes(0)
+ , fRowsDecoded(0)
+{
+ reader->setClient(this);
}
-/*
- * Assumes IsGif was called and returned true
- * Creates a gif decoder
- * Reads enough of the stream to determine the image format
- */
-SkCodec* SkGifCodec::NewFromStream(SkStream* stream) {
- SkCodec* codec = nullptr;
- if (ReadHeader(stream, &codec, nullptr)) {
- return codec;
+std::vector<SkCodec::FrameInfo> SkGifCodec::onGetFrameInfo() {
+ fReader->parse(GIFImageReader::GIFFrameCountQuery);
+ const size_t size = fReader->imagesCount();
+ std::vector<FrameInfo> result(size);
+ for (size_t i = 0; i < size; i++) {
+ const GIFFrameContext* frameContext = fReader->frameContext(i);
+ result[i].fDuration = frameContext->delayTime();
+ result[i].fRequiredFrame = frameContext->getRequiredFrame();
}
- return nullptr;
+ return result;
}
-SkGifCodec::SkGifCodec(int width, int height, const SkEncodedInfo& info, SkStream* stream,
- GifFileType* gif, uint32_t transIndex, const SkIRect& frameRect, bool frameIsSubset)
- : INHERITED(width, height, info, stream)
- , fGif(gif)
- , fSrcBuffer(new uint8_t[this->getInfo().width()])
- , fFrameRect(frameRect)
- // If it is valid, fTransIndex will be used to set fFillIndex. We don't know if
- // fTransIndex is valid until we process the color table, since fTransIndex may
- // be greater than the size of the color table.
- , fTransIndex(transIndex)
- // Default fFillIndex is 0. We will overwrite this if fTransIndex is valid, or if
- // there is a valid background color.
- , fFillIndex(0)
- , fFrameIsSubset(frameIsSubset)
- , fSwizzler(NULL)
- , fColorTable(NULL)
-{}
+void SkGifCodec::initializeColorTable(const SkImageInfo& dstInfo, size_t frameIndex,
+ SkPMColor* inputColorPtr, int* inputColorCount) {
+ fCurrColorTable = fReader->getColorTable(dstInfo.colorType(), frameIndex);
+ fCurrColorTableIsReal = fCurrColorTable;
+ if (!fCurrColorTable) {
+ // This is possible for an empty frame. Create a dummy with one value (transparent).
+ SkPMColor color = SK_ColorTRANSPARENT;
+ fCurrColorTable.reset(new SkColorTable(&color, 1));
+ }
-bool SkGifCodec::onRewind() {
- GifFileType* gifOut = nullptr;
- if (!ReadHeader(this->stream(), nullptr, &gifOut)) {
- return false;
+ if (inputColorCount) {
+ *inputColorCount = fCurrColorTable->count();
}
- SkASSERT(nullptr != gifOut);
- fGif.reset(gifOut);
- return true;
+ copy_color_table(dstInfo, fCurrColorTable.get(), inputColorPtr, inputColorCount);
}
-SkCodec::Result SkGifCodec::ReadUpToFirstImage(GifFileType* gif, uint32_t* transIndex) {
- // Use this as a container to hold information about any gif extension
- // blocks. This generally stores transparency and animation instructions.
- SavedImage saveExt;
- SkAutoTCallVProc<SavedImage, FreeExtension> autoFreeExt(&saveExt);
- saveExt.ExtensionBlocks = nullptr;
- saveExt.ExtensionBlockCount = 0;
- GifByteType* extData;
- int32_t extFunction;
-
- // We will loop over components of gif images until we find an image. Once
- // we find an image, we will decode and return it. While many gif files
- // contain more than one image, we will simply decode the first image.
- GifRecordType recordType;
- do {
- // Get the current record type
- if (GIF_ERROR == DGifGetRecordType(gif, &recordType)) {
- return gif_error("DGifGetRecordType failed.\n", kInvalidInput);
- }
- switch (recordType) {
- case IMAGE_DESC_RECORD_TYPE: {
- *transIndex = find_trans_index(saveExt);
-
- // FIXME: Gif files may have multiple images stored in a single
- // file. This is most commonly used to enable
- // animations. Since we are leaving animated gifs as a
- // TODO, we will return kSuccess after decoding the
- // first image in the file. This is the same behavior
- // as SkImageDecoder_libgif.
- //
- // Most times this works pretty well, but sometimes it
- // doesn't. For example, I have an animated test image
- // where the first image in the file is 1x1, but the
- // subsequent images are meaningful. This currently
- // displays the 1x1 image, which is not ideal. Right
- // now I am leaving this as an issue that will be
- // addressed when we implement animated gifs.
- //
- // It is also possible (not explicitly disallowed in the
- // specification) that gif files provide multiple
- // images in a single file that are all meant to be
- // displayed in the same frame together. I will
- // currently leave this unimplemented until I find a
- // test case that expects this behavior.
- return kSuccess;
- }
- // Extensions are used to specify special properties of the image
- // such as transparency or animation.
- case EXTENSION_RECORD_TYPE:
- // Read extension data
- if (GIF_ERROR == DGifGetExtension(gif, &extFunction, &extData)) {
- return gif_error("Could not get extension.\n", kIncompleteInput);
- }
-
- // Create an extension block with our data
- while (nullptr != extData) {
- // Add a single block
-
-#if GIFLIB_MAJOR < 5
- if (AddExtensionBlock(&saveExt, extData[0],
- &extData[1]) == GIF_ERROR) {
-#else
- if (GIF_ERROR == GifAddExtensionBlock(&saveExt.ExtensionBlockCount,
- &saveExt.ExtensionBlocks,
- extFunction, extData[0], &extData[1])) {
-#endif
- return gif_error("Could not add extension block.\n", kIncompleteInput);
- }
- // Move to the next block
- if (GIF_ERROR == DGifGetExtensionNext(gif, &extData)) {
- return gif_error("Could not get next extension.\n", kIncompleteInput);
- }
- }
- break;
-
- // Signals the end of the gif file
- case TERMINATE_RECORD_TYPE:
- break;
-
- default:
- // DGifGetRecordType returns an error if the record type does
- // not match one of the above cases. This should not be
- // reached.
- SkASSERT(false);
- break;
- }
- } while (TERMINATE_RECORD_TYPE != recordType);
-
- return gif_error("Could not find any images to decode in gif file.\n", kInvalidInput);
-}
-bool SkGifCodec::GetDimensions(GifFileType* gif, SkISize* size, SkIRect* frameRect) {
- // Get the encoded dimension values
- SavedImage* image = &gif->SavedImages[gif->ImageCount - 1];
- const GifImageDesc& desc = image->ImageDesc;
- int frameLeft = desc.Left;
- int frameTop = desc.Top;
- int frameWidth = desc.Width;
- int frameHeight = desc.Height;
- int width = gif->SWidth;
- int height = gif->SHeight;
-
- // Ensure that the decode dimensions are large enough to contain the frame
- width = SkTMax(width, frameWidth + frameLeft);
- height = SkTMax(height, frameHeight + frameTop);
-
- // All of these dimensions should be positive, as they are encoded as unsigned 16-bit integers.
- // It is unclear why giflib casts them to ints. We will go ahead and check that they are
- // in fact positive.
- if (frameLeft < 0 || frameTop < 0 || frameWidth < 0 || frameHeight < 0 || width <= 0 ||
- height <= 0) {
- return false;
+SkCodec::Result SkGifCodec::prepareToDecode(const SkImageInfo& dstInfo, SkPMColor* inputColorPtr,
+ int* inputColorCount, const Options& opts) {
+ // Check for valid input parameters
+ if (!conversion_possible_ignore_color_space(dstInfo, this->getInfo())) {
+ return gif_error("Cannot convert input type to output type.\n", kInvalidConversion);
}
- frameRect->setXYWH(frameLeft, frameTop, frameWidth, frameHeight);
- size->set(width, height);
- return true;
-}
-
-void SkGifCodec::initializeColorTable(const SkImageInfo& dstInfo, SkPMColor* inputColorPtr,
- int* inputColorCount) {
- // Set up our own color table
- const uint32_t maxColors = 256;
- SkPMColor colorPtr[256];
- if (NULL != inputColorCount) {
- // We set the number of colors to maxColors in order to ensure
- // safe memory accesses. Otherwise, an invalid pixel could
- // access memory outside of our color table array.
- *inputColorCount = maxColors;
+ if (dstInfo.colorType() == kRGBA_F16_SkColorType) {
+ // FIXME: This should be supported.
+ return gif_error("GIF does not yet support F16.\n", kInvalidConversion);
}
- // Get local color table
- ColorMapObject* colorMap = fGif->Image.ColorMap;
- // If there is no local color table, use the global color table
- if (NULL == colorMap) {
- colorMap = fGif->SColorMap;
+ if (opts.fSubset) {
+ return gif_error("Subsets not supported.\n", kUnimplemented);
}
- uint32_t colorCount = 0;
- if (NULL != colorMap) {
- colorCount = colorMap->ColorCount;
- // giflib guarantees these properties
- SkASSERT(colorCount == (unsigned) (1 << (colorMap->BitsPerPixel)));
- SkASSERT(colorCount <= 256);
- PackColorProc proc = choose_pack_color_proc(false, dstInfo.colorType());
- for (uint32_t i = 0; i < colorCount; i++) {
- colorPtr[i] = proc(0xFF, colorMap->Colors[i].Red,
- colorMap->Colors[i].Green, colorMap->Colors[i].Blue);
- }
+ const size_t frameIndex = opts.fFrameIndex;
+ if (frameIndex > 0 && dstInfo.colorType() == kIndex_8_SkColorType) {
+ // FIXME: It is possible that a later frame can be decoded to index8, if it does one of the
+ // following:
+ // - Covers the entire previous frame
+ // - Shares a color table (and transparent index) with any prior frames that are showing.
+ // We must support index8 for the first frame to be backwards compatible on Android, but
+ // we do not (currently) need to support later frames as index8.
+ return gif_error("Cannot decode multiframe gif (except frame 0) as index 8.\n",
+ kInvalidConversion);
}
- // Fill in the color table for indices greater than color count.
- // This allows for predictable, safe behavior.
- if (colorCount > 0) {
- // Gifs have the option to specify the color at a single index of the color
- // table as transparent. If the transparent index is greater than the
- // colorCount, we know that there is no valid transparent color in the color
- // table. If there is not valid transparent index, we will try to use the
- // backgroundIndex as the fill index. If the backgroundIndex is also not
- // valid, we will let fFillIndex default to 0 (it is set to zero in the
- // constructor). This behavior is not specified but matches
- // SkImageDecoder_libgif.
- uint32_t backgroundIndex = fGif->SBackGroundColor;
- if (fTransIndex < colorCount) {
- colorPtr[fTransIndex] = SK_ColorTRANSPARENT;
- fFillIndex = fTransIndex;
- } else if (backgroundIndex < colorCount) {
- fFillIndex = backgroundIndex;
- }
+ fReader->parse((GIFImageReader::GIFParseQuery) frameIndex);
- for (uint32_t i = colorCount; i < maxColors; i++) {
- colorPtr[i] = colorPtr[fFillIndex];
- }
- } else {
- sk_memset32(colorPtr, 0xFF000000, maxColors);
+ if (frameIndex >= fReader->imagesCount()) {
+ return gif_error("frame index out of range!\n", kIncompleteInput);
}
- fColorTable.reset(new SkColorTable(colorPtr, maxColors));
- copy_color_table(dstInfo, this->fColorTable, inputColorPtr, inputColorCount);
-}
-
-SkCodec::Result SkGifCodec::prepareToDecode(const SkImageInfo& dstInfo, SkPMColor* inputColorPtr,
- int* inputColorCount, const Options& opts) {
- // Check for valid input parameters
- if (!conversion_possible_ignore_color_space(dstInfo, this->getInfo())) {
- return gif_error("Cannot convert input type to output type.\n", kInvalidConversion);
- }
+ fTmpBuffer.reset(new uint8_t[dstInfo.minRowBytes()]);
// Initialize color table and copy to the client if necessary
- this->initializeColorTable(dstInfo, inputColorPtr, inputColorCount);
-
- this->initializeSwizzler(dstInfo, opts);
+ this->initializeColorTable(dstInfo, frameIndex, inputColorPtr, inputColorCount);
+ this->initializeSwizzler(dstInfo, frameIndex);
return kSuccess;
}
-void SkGifCodec::initializeSwizzler(const SkImageInfo& dstInfo, const Options& opts) {
- const SkPMColor* colorPtr = get_color_ptr(fColorTable.get());
- const SkIRect* frameRect = fFrameIsSubset ? &fFrameRect : nullptr;
- fSwizzler.reset(SkSwizzler::CreateSwizzler(this->getEncodedInfo(), colorPtr, dstInfo, opts,
- frameRect));
- SkASSERT(fSwizzler);
-}
-
-bool SkGifCodec::readRow() {
- return GIF_ERROR != DGifGetLine(fGif, fSrcBuffer.get(), fFrameRect.width());
+void SkGifCodec::initializeSwizzler(const SkImageInfo& dstInfo, size_t frameIndex) {
+ const GIFFrameContext* frame = fReader->frameContext(frameIndex);
+ // This is only called by prepareToDecode, which ensures frameIndex is in range.
+ SkASSERT(frame);
+
+ const int xBegin = frame->xOffset();
+ const int xEnd = std::min(static_cast<int>(frame->xOffset() + frame->width()),
+ static_cast<int>(fReader->screenWidth()));
+
+ // CreateSwizzler only reads left and right of the frame. We cannot use the frame's raw
+ // frameRect, since it might extend beyond the edge of the frame.
+ SkIRect swizzleRect = SkIRect::MakeLTRB(xBegin, 0, xEnd, 0);
+
+ // The default Options should be fine:
+ // - we'll ignore if the memory is zero initialized - unless we're the first frame, this won't
+ // matter anyway.
+ // - subsets are not supported for gif
+ // - the swizzler does not need to know about the frame.
+ // We may not be able to use the real Options anyway, since getPixels does not store it (due to
+ // a bug).
+ fSwizzler.reset(SkSwizzler::CreateSwizzler(this->getEncodedInfo(),
+ fCurrColorTable->readColors(), dstInfo, Options(), &swizzleRect));
+ SkASSERT(fSwizzler.get());
}
/*
* Initiates the gif decode
*/
SkCodec::Result SkGifCodec::onGetPixels(const SkImageInfo& dstInfo,
- void* dst, size_t dstRowBytes,
+ void* pixels, size_t dstRowBytes,
const Options& opts,
SkPMColor* inputColorPtr,
int* inputColorCount,
return gif_error("Scaling not supported.\n", kInvalidScale);
}
- // Initialize the swizzler
- if (fFrameIsSubset) {
- // Fill the background
- SkSampler::Fill(dstInfo, dst, dstRowBytes, this->getFillValue(dstInfo),
- opts.fZeroInitialized);
- }
+ fDst = pixels;
+ fDstRowBytes = dstRowBytes;
- // Iterate over rows of the input
- for (int y = fFrameRect.top(); y < fFrameRect.bottom(); y++) {
- if (!this->readRow()) {
- *rowsDecoded = y;
- return gif_error("Could not decode line.\n", kIncompleteInput);
- }
- void* dstRow = SkTAddOffset<void>(dst, dstRowBytes * this->outputScanline(y));
- fSwizzler->swizzle(dstRow, fSrcBuffer.get());
+ return this->decodeFrame(true, opts, rowsDecoded);
+}
+
+SkCodec::Result SkGifCodec::onStartIncrementalDecode(const SkImageInfo& dstInfo,
+ void* pixels, size_t dstRowBytes,
+ const SkCodec::Options& opts,
+ SkPMColor* inputColorPtr,
+ int* inputColorCount) {
+ Result result = this->prepareToDecode(dstInfo, inputColorPtr, inputColorCount, opts);
+ if (result != kSuccess) {
+ return result;
}
+
+ fDst = pixels;
+ fDstRowBytes = dstRowBytes;
+
+ fFirstCallToIncrementalDecode = true;
+
return kSuccess;
}
-// FIXME: This is similar to the implementation for bmp and png. Can we share more code or
-// possibly make this non-virtual?
-uint64_t SkGifCodec::onGetFillValue(const SkImageInfo& dstInfo) const {
- const SkPMColor* colorPtr = get_color_ptr(fColorTable.get());
- return get_color_table_fill_value(dstInfo.colorType(), dstInfo.alphaType(), colorPtr,
- fFillIndex, nullptr);
-}
+SkCodec::Result SkGifCodec::onIncrementalDecode(int* rowsDecoded) {
+ // It is possible the client has appended more data. Parse, if needed.
+ const auto& options = this->options();
+ const size_t frameIndex = options.fFrameIndex;
+ fReader->parse((GIFImageReader::GIFParseQuery) frameIndex);
-SkCodec::Result SkGifCodec::onStartScanlineDecode(const SkImageInfo& dstInfo,
- const SkCodec::Options& opts, SkPMColor inputColorPtr[], int* inputColorCount) {
- return this->prepareToDecode(dstInfo, inputColorPtr, inputColorCount, opts);
+ const bool firstCallToIncrementalDecode = fFirstCallToIncrementalDecode;
+ fFirstCallToIncrementalDecode = false;
+ return this->decodeFrame(firstCallToIncrementalDecode, options, rowsDecoded);
}
-void SkGifCodec::handleScanlineFrame(int count, int* rowsBeforeFrame, int* rowsInFrame) {
- if (fFrameIsSubset) {
- const int currRow = this->currScanline();
+SkCodec::Result SkGifCodec::decodeFrame(bool firstAttempt, const Options& opts, int* rowsDecoded) {
+ const SkImageInfo& dstInfo = this->dstInfo();
+ const size_t frameIndex = opts.fFrameIndex;
+ SkASSERT(frameIndex < fReader->imagesCount());
+ const GIFFrameContext* frameContext = fReader->frameContext(frameIndex);
+ if (firstAttempt) {
+ // rowsDecoded reports how many rows have been initialized, so a layer above
+ // can fill the rest. In some cases, we fill the background before decoding
+ // (or it is already filled for us), so we report rowsDecoded to be the full
+ // height.
+ bool filledBackground = false;
+ if (frameContext->getRequiredFrame() == kNone) {
+ // We may need to clear to transparent for one of the following reasons:
+ // - The frameRect does not cover the full bounds. haveDecodedRow will
+ // only draw inside the frameRect, so we need to clear the rest.
+ // - There is a valid transparent pixel value. (FIXME: I'm assuming
+ // writeTransparentPixels will be false in this case, based on
+ // Chromium's assumption that it would already be zeroed. If we
+ // change that behavior, could we skip Filling here?)
+ // - The frame is interlaced. There is no obvious way to fill
+ // afterwards for an incomplete image. (FIXME: Does the first pass
+ // cover all rows? If so, we do not have to fill here.)
+ if (frameContext->frameRect() != this->getInfo().bounds()
+ || frameContext->transparentPixel() < MAX_COLORS
+ || frameContext->interlaced()) {
+ // fill ignores the width (replaces it with the actual, scaled width).
+ // But we need to scale in Y.
+ const int scaledHeight = get_scaled_dimension(dstInfo.height(),
+ fSwizzler->sampleY());
+ auto fillInfo = dstInfo.makeWH(0, scaledHeight);
+ fSwizzler->fill(fillInfo, fDst, fDstRowBytes, this->getFillValue(dstInfo),
+ opts.fZeroInitialized);
+ filledBackground = true;
+ }
+ } else {
+ // Not independent
+ if (!opts.fHasPriorFrame) {
+ // Decode that frame into pixels.
+ Options prevFrameOpts(opts);
+ prevFrameOpts.fFrameIndex = frameContext->getRequiredFrame();
+ prevFrameOpts.fHasPriorFrame = false;
+ const Result prevResult = this->decodeFrame(true, prevFrameOpts, nullptr);
+ switch (prevResult) {
+ case kSuccess:
+ // Prior frame succeeded. Carry on.
+ break;
+ case kIncompleteInput:
+ // Prior frame was incomplete. So this frame cannot be decoded.
+ return kInvalidInput;
+ default:
+ return prevResult;
+ }
+ }
+ const auto* prevFrame = fReader->frameContext(frameContext->getRequiredFrame());
+ if (prevFrame->getDisposalMethod() == SkCodecAnimation::RestoreBGColor_DisposalMethod) {
+ const SkIRect prevRect = prevFrame->frameRect();
+ auto left = get_scaled_dimension(prevRect.fLeft, fSwizzler->sampleX());
+ auto top = get_scaled_dimension(prevRect.fTop, fSwizzler->sampleY());
+ void* const eraseDst = SkTAddOffset<void>(fDst, top * fDstRowBytes
+ + left * SkColorTypeBytesPerPixel(dstInfo.colorType()));
+ auto width = get_scaled_dimension(prevRect.width(), fSwizzler->sampleX());
+ auto height = get_scaled_dimension(prevRect.height(), fSwizzler->sampleY());
+ // fSwizzler->fill() would fill to the scaled width of the frame, but we want to
+ // fill to the scaled with of the width of the PRIOR frame, so we do all the scaling
+ // ourselves and call the static version.
+ SkSampler::Fill(dstInfo.makeWH(width, height), eraseDst,
+ fDstRowBytes, this->getFillValue(dstInfo), kNo_ZeroInitialized);
+ }
+ filledBackground = true;
+ }
- // The number of rows that remain to be skipped before reaching rows that we
- // actually must decode into.
- // This must be at least zero. We also make sure that it is less than or
- // equal to count, since we will skip at most count rows.
- *rowsBeforeFrame = SkTMin(count, SkTMax(0, fFrameRect.top() - currRow));
+ fFilledBackground = filledBackground;
+ if (filledBackground) {
+ // Report the full (scaled) height, since the client will never need to fill.
+ fRowsDecoded = get_scaled_dimension(dstInfo.height(), fSwizzler->sampleY());
+ } else {
+ // This will be updated by haveDecodedRow.
+ fRowsDecoded = 0;
+ }
+ }
- // Rows left to decode once we reach the start of the frame.
- const int rowsLeft = count - *rowsBeforeFrame;
+ // Note: there is a difference between the following call to GIFImageReader::decode
+ // returning false and leaving frameDecoded false:
+ // - If the method returns false, there was an error in the stream. We still treat this as
+ // incomplete, since we have already decoded some rows.
+ // - If frameDecoded is false, that just means that we do not have enough data. If more data
+ // is supplied, we may be able to continue decoding this frame. We also treat this as
+ // incomplete.
+ // FIXME: Ensure that we do not attempt to continue decoding if the method returns false and
+ // more data is supplied.
+ bool frameDecoded = false;
+ if (!fReader->decode(frameIndex, &frameDecoded) || !frameDecoded) {
+ if (rowsDecoded) {
+ *rowsDecoded = fRowsDecoded;
+ }
+ return kIncompleteInput;
+ }
- // Count the number of that extend beyond the bottom of the frame. We do not
- // need to decode into these rows.
- const int rowsAfterFrame = SkTMax(0, currRow + rowsLeft - fFrameRect.bottom());
+ return kSuccess;
+}
- // Set the actual number of source rows that we need to decode.
- *rowsInFrame = rowsLeft - rowsAfterFrame;
- } else {
- *rowsBeforeFrame = 0;
- *rowsInFrame = count;
+uint64_t SkGifCodec::onGetFillValue(const SkImageInfo& dstInfo) const {
+ // Note: Using fCurrColorTable relies on having called initializeColorTable already.
+ // This is (currently) safe because this method is only called when filling, after
+ // initializeColorTable has been called.
+ // FIXME: Is there a way to make this less fragile?
+ if (dstInfo.colorType() == kIndex_8_SkColorType && fCurrColorTableIsReal) {
+ // We only support index 8 for the first frame, for backwards
+ // compatibity on Android, so we are using the color table for the first frame.
+ SkASSERT(this->options().fFrameIndex == 0);
+ // Use the transparent index for the first frame.
+ const size_t transPixel = fReader->frameContext(0)->transparentPixel();
+ if (transPixel < (size_t) fCurrColorTable->count()) {
+ return transPixel;
+ }
+ // Fall through to return SK_ColorTRANSPARENT (i.e. 0). This choice is arbitrary,
+ // but we have to pick something inside the color table, and this one is as good
+ // as any.
}
+ // Using transparent as the fill value matches the behavior in Chromium,
+ // which ignores the background color.
+ // If the colorType is kIndex_8, and there was no color table (i.e.
+ // fCurrColorTableIsReal is false), this value (zero) corresponds to the
+ // only entry in the dummy color table provided to the client.
+ return SK_ColorTRANSPARENT;
}
-int SkGifCodec::onGetScanlines(void* dst, int count, size_t rowBytes) {
- int rowsBeforeFrame;
- int rowsInFrame;
- this->handleScanlineFrame(count, &rowsBeforeFrame, &rowsInFrame);
+bool SkGifCodec::haveDecodedRow(size_t frameIndex, const unsigned char* rowBegin,
+ size_t rowNumber, unsigned repeatCount, bool writeTransparentPixels)
+{
+ const GIFFrameContext* frameContext = fReader->frameContext(frameIndex);
+ // The pixel data and coordinates supplied to us are relative to the frame's
+ // origin within the entire image size, i.e.
+ // (frameContext->xOffset, frameContext->yOffset). There is no guarantee
+ // that width == (size().width() - frameContext->xOffset), so
+ // we must ensure we don't run off the end of either the source data or the
+ // row's X-coordinates.
+ const size_t width = frameContext->width();
+ const int xBegin = frameContext->xOffset();
+ const int yBegin = frameContext->yOffset() + rowNumber;
+ const int xEnd = std::min(static_cast<int>(frameContext->xOffset() + width),
+ this->getInfo().width());
+ const int yEnd = std::min(static_cast<int>(frameContext->yOffset() + rowNumber + repeatCount),
+ this->getInfo().height());
+ // FIXME: No need to make the checks on width/xBegin/xEnd for every row. We could instead do
+ // this once in prepareToDecode.
+ if (!width || (xBegin < 0) || (yBegin < 0) || (xEnd <= xBegin) || (yEnd <= yBegin))
+ return true;
+
+ // yBegin is the first row in the non-sampled image. dstRow will be the row in the output,
+ // after potentially scaling it.
+ int dstRow = yBegin;
+
+ const int sampleY = fSwizzler->sampleY();
+ if (sampleY > 1) {
+ // Check to see whether this row or one that falls in the repeatCount is needed in the
+ // output.
+ bool foundNecessaryRow = false;
+ for (unsigned i = 0; i < repeatCount; i++) {
+ const int potentialRow = yBegin + i;
+ if (fSwizzler->rowNeeded(potentialRow)) {
+ dstRow = potentialRow / sampleY;
+ const int scaledHeight = get_scaled_dimension(this->dstInfo().height(), sampleY);
+ if (dstRow >= scaledHeight) {
+ return true;
+ }
- if (fFrameIsSubset) {
- // Fill the requested rows
- SkImageInfo fillInfo = this->dstInfo().makeWH(this->dstInfo().width(), count);
- uint64_t fillValue = this->onGetFillValue(this->dstInfo());
- fSwizzler->fill(fillInfo, dst, rowBytes, fillValue, this->options().fZeroInitialized);
+ foundNecessaryRow = true;
+ repeatCount -= i;
- // Start to write pixels at the start of the image frame
- dst = SkTAddOffset<void>(dst, rowBytes * rowsBeforeFrame);
- }
+ repeatCount = (repeatCount - 1) / sampleY + 1;
- for (int i = 0; i < rowsInFrame; i++) {
- if (!this->readRow()) {
- return i + rowsBeforeFrame;
+ // Make sure the repeatCount does not take us beyond the end of the dst
+ if (dstRow + (int) repeatCount > scaledHeight) {
+ repeatCount = scaledHeight - dstRow;
+ SkASSERT(repeatCount >= 1);
+ }
+ break;
+ }
}
- fSwizzler->swizzle(dst, fSrcBuffer.get());
- dst = SkTAddOffset<void>(dst, rowBytes);
- }
-
- return count;
-}
-
-bool SkGifCodec::onSkipScanlines(int count) {
- int rowsBeforeFrame;
- int rowsInFrame;
- this->handleScanlineFrame(count, &rowsBeforeFrame, &rowsInFrame);
- for (int i = 0; i < rowsInFrame; i++) {
- if (!this->readRow()) {
- return false;
+ if (!foundNecessaryRow) {
+ return true;
}
}
- return true;
-}
+ if (!fFilledBackground) {
+ // At this point, we are definitely going to write the row, so count it towards the number
+ // of rows decoded.
+ // We do not consider the repeatCount, which only happens for interlaced, in which case we
+ // have already set fRowsDecoded to the proper value (reflecting that we have filled the
+ // background).
+ fRowsDecoded++;
+ }
-SkCodec::SkScanlineOrder SkGifCodec::onGetScanlineOrder() const {
- if (fGif->Image.Interlace) {
- return kOutOfOrder_SkScanlineOrder;
+ if (!fCurrColorTableIsReal) {
+ // No color table, so nothing to draw this frame.
+ // FIXME: We can abort even earlier - no need to decode this frame.
+ return true;
}
- return kTopDown_SkScanlineOrder;
-}
-int SkGifCodec::onOutputScanline(int inputScanline) const {
- if (fGif->Image.Interlace) {
- if (inputScanline < fFrameRect.top() || inputScanline >= fFrameRect.bottom()) {
- return inputScanline;
+ // The swizzler takes care of offsetting into the dst width-wise.
+ void* dstLine = SkTAddOffset<void>(fDst, dstRow * fDstRowBytes);
+
+ // We may or may not need to write transparent pixels to the buffer.
+ // If we're compositing against a previous image, it's wrong, and if
+ // we're writing atop a cleared, fully transparent buffer, it's
+ // unnecessary; but if we're decoding an interlaced gif and
+ // displaying it "Haeberli"-style, we must write these for passes
+ // beyond the first, or the initial passes will "show through" the
+ // later ones.
+ const auto dstInfo = this->dstInfo();
+ if (writeTransparentPixels || dstInfo.colorType() == kRGB_565_SkColorType) {
+ fSwizzler->swizzle(dstLine, rowBegin);
+ } else {
+ // We cannot swizzle directly into the dst, since that will write the transparent pixels.
+ // Instead, swizzle into a temporary buffer, and copy that into the dst.
+ {
+ void* const memsetDst = fTmpBuffer.get();
+ // Although onGetFillValue returns a uint64_t, we only use the low eight bits. The
+ // return value is either an 8 bit index (for index8) or SK_ColorTRANSPARENT, which is
+ // all zeroes.
+ const int fillValue = (uint8_t) this->onGetFillValue(dstInfo);
+ const size_t rb = dstInfo.minRowBytes();
+ if (fillValue == 0) {
+ // FIXME: This special case should be unnecessary, and in fact sk_bzero just calls
+ // memset. But without it, the compiler thinks this is trying to pass a zero length
+ // to memset, causing an error.
+ sk_bzero(memsetDst, rb);
+ } else {
+ memset(memsetDst, fillValue, rb);
+ }
+ }
+ fSwizzler->swizzle(fTmpBuffer.get(), rowBegin);
+
+ const size_t offsetBytes = fSwizzler->swizzleOffsetBytes();
+ switch (dstInfo.colorType()) {
+ case kBGRA_8888_SkColorType:
+ case kRGBA_8888_SkColorType: {
+ uint32_t* dstPixel = SkTAddOffset<uint32_t>(dstLine, offsetBytes);
+ uint32_t* srcPixel = SkTAddOffset<uint32_t>(fTmpBuffer.get(), offsetBytes);
+ for (int i = 0; i < fSwizzler->swizzleWidth(); i++) {
+ // Technically SK_ColorTRANSPARENT is an SkPMColor, and srcPixel would have
+ // the opposite swizzle for the non-native swizzle, but TRANSPARENT is all
+ // zeroes, which is the same either way.
+ if (*srcPixel != SK_ColorTRANSPARENT) {
+ *dstPixel = *srcPixel;
+ }
+ dstPixel++;
+ srcPixel++;
+ }
+ break;
+ }
+ case kIndex_8_SkColorType: {
+ uint8_t* dstPixel = SkTAddOffset<uint8_t>(dstLine, offsetBytes);
+ uint8_t* srcPixel = SkTAddOffset<uint8_t>(fTmpBuffer.get(), offsetBytes);
+ for (int i = 0; i < fSwizzler->swizzleWidth(); i++) {
+ if (*srcPixel != frameContext->transparentPixel()) {
+ *dstPixel = *srcPixel;
+ }
+ dstPixel++;
+ srcPixel++;
+ }
+ break;
+ }
+ default:
+ SkASSERT(false);
+ break;
}
- return get_output_row_interlaced(inputScanline - fFrameRect.top(), fFrameRect.height()) +
- fFrameRect.top();
}
- return inputScanline;
+
+ // Tell the frame to copy the row data if need be.
+ if (repeatCount > 1) {
+ const size_t bytesPerPixel = SkColorTypeBytesPerPixel(this->dstInfo().colorType());
+ const size_t bytesToCopy = fSwizzler->swizzleWidth() * bytesPerPixel;
+ void* copiedLine = SkTAddOffset<void>(dstLine, fSwizzler->swizzleOffsetBytes());
+ void* dst = copiedLine;
+ for (unsigned i = 1; i < repeatCount; i++) {
+ dst = SkTAddOffset<void>(dst, fDstRowBytes);
+ memcpy(dst, copiedLine, bytesToCopy);
+ }
+ }
+
+ return true;
}
*/
#include "SkCodec.h"
+#include "SkCodecAnimation.h"
#include "SkColorSpace.h"
#include "SkColorTable.h"
#include "SkImageInfo.h"
#include "SkSwizzler.h"
-struct GifFileType;
-struct SavedImage;
+#include "GIFImageReader.h"
/*
*
*/
static SkCodec* NewFromStream(SkStream*);
+ // Callback for GIFImageReader when a row is available.
+ bool haveDecodedRow(size_t frameIndex, const unsigned char* rowBegin,
+ size_t rowNumber, unsigned repeatCount, bool writeTransparentPixels);
protected:
-
- /*
- * Read enough of the stream to initialize the SkGifCodec.
- * Returns a bool representing success or failure.
- *
- * @param codecOut
- * If it returned true, and codecOut was not nullptr,
- * codecOut will be set to a new SkGifCodec.
- *
- * @param gifOut
- * If it returned true, and codecOut was nullptr,
- * gifOut must be non-nullptr and gifOut will be set to a new
- * GifFileType pointer.
- *
- * @param stream
- * Deleted on failure.
- * codecOut will take ownership of it in the case where we created a codec.
- * Ownership is unchanged when we returned a gifOut.
- *
- */
- static bool ReadHeader(SkStream* stream, SkCodec** codecOut,
- GifFileType** gifOut);
-
/*
* Performs the full gif decode
*/
uint64_t onGetFillValue(const SkImageInfo&) const override;
- int onOutputScanline(int inputScanline) const override;
+ std::vector<FrameInfo> onGetFrameInfo() override;
-private:
+ Result onStartIncrementalDecode(const SkImageInfo& /*dstInfo*/, void*, size_t,
+ const SkCodec::Options&, SkPMColor*, int*) override;
- /*
- * A gif can contain multiple image frames. We will only decode the first
- * frame. This function reads up to the first image frame, processing
- * transparency and/or animation information that comes before the image
- * data.
- *
- * @param gif Pointer to the library type that manages the gif decode
- * @param transIndex This call will set the transparent index based on the
- * extension data.
- */
- static Result ReadUpToFirstImage(GifFileType* gif, uint32_t* transIndex);
-
- /*
- * A gif may contain many image frames, all of different sizes.
- * This function checks if the gif dimensions are valid, based on the frame
- * dimensions, and corrects the gif dimensions if necessary.
- *
- * @param gif Pointer to the library type that manages the gif decode
- * @param size Size of the image that we will decode.
- * Will be set by this function if the return value is true.
- * @param frameRect Contains the dimenions and offset of the first image frame.
- * Will be set by this function if the return value is true.
- *
- * @return true on success, false otherwise
- */
- static bool GetDimensions(GifFileType* gif, SkISize* size, SkIRect* frameRect);
+ Result onIncrementalDecode(int*) override;
+
+private:
/*
* Initializes the color table that we will use for decoding.
*
* @param dstInfo Contains the requested dst color type.
+ * @param frameIndex Frame whose color table to use.
* @param inputColorPtr Copies the encoded color table to the client's
* input color table if the client requests kIndex8.
- * @param inputColorCount If the client requests kIndex8, sets
- * inputColorCount to 256. Since gifs always
- * contain 8-bit indices, we need a 256 entry color
- * table to ensure that indexing is always in
- * bounds.
+ * @param inputColorCount If the client requests kIndex8, this will be set
+ * to the number of colors in the array that
+ * inputColorPtr now points to. This will typically
+ * be 256. Since gifs may have up to 8-bit indices,
+ * using a 256-entry table means a pixel will never
+ * be out of range. This will be set to 1 if there
+ * is no color table, since that will be a
+ * transparent frame.
*/
- void initializeColorTable(const SkImageInfo& dstInfo, SkPMColor* colorPtr,
- int* inputColorCount);
+ void initializeColorTable(const SkImageInfo& dstInfo, size_t frameIndex,
+ SkPMColor* colorPtr, int* inputColorCount);
/*
- * Checks for invalid inputs and calls setFrameDimensions(), and
- * initializeColorTable() in the proper sequence.
+ * Does necessary setup, including setting up the color table and swizzler,
+ * and reports color info to the client.
*/
Result prepareToDecode(const SkImageInfo& dstInfo, SkPMColor* inputColorPtr,
int* inputColorCount, const Options& opts);
/*
* Initializes the swizzler.
*
- * @param dstInfo Output image information. Dimensions may have been
- * adjusted if the image frame size does not match the size
- * indicated in the header.
- * @param options Informs the swizzler if destination memory is zero initialized.
- * Contains subset information.
+ * @param dstInfo Output image information. Dimensions may have been
+ * adjusted if the image frame size does not match the size
+ * indicated in the header.
+ * @param frameIndex Which frame we are decoding. This determines the frameRect
+ * to use.
*/
- void initializeSwizzler(const SkImageInfo& dstInfo,
- const Options& options);
+ void initializeSwizzler(const SkImageInfo& dstInfo, size_t frameIndex);
SkSampler* getSampler(bool createIfNecessary) override {
SkASSERT(fSwizzler);
- return fSwizzler;
+ return fSwizzler.get();
}
/*
- * @return true if the read is successful and false if the read fails.
- */
- bool readRow();
-
- Result onStartScanlineDecode(const SkImageInfo& dstInfo, const Options& opts,
- SkPMColor inputColorPtr[], int* inputColorCount) override;
-
- int onGetScanlines(void* dst, int count, size_t rowBytes) override;
-
- bool onSkipScanlines(int count) override;
-
- /*
- * For a scanline decode of "count" lines, this function indicates how
- * many of the "count" lines should be skipped until we reach the top of
- * the image frame and how many of the "count" lines are actually inside
- * the image frame.
+ * Recursive function to decode a frame.
*
- * @param count The number of scanlines requested.
- * @param rowsBeforeFrame Output variable. The number of lines before
- * we reach the top of the image frame.
- * @param rowsInFrame Output variable. The number of lines to decode
- * inside the image frame.
- */
- void handleScanlineFrame(int count, int* rowsBeforeFrame, int* rowsInFrame);
-
- SkScanlineOrder onGetScanlineOrder() const override;
-
- /*
- * This function cleans up the gif object after the decode completes
- * It is used in a SkAutoTCallIProc template
+ * @param firstAttempt Whether this is the first call to decodeFrame since
+ * starting. e.g. true in onGetPixels, and true in the
+ * first call to onIncrementalDecode after calling
+ * onStartIncrementalDecode.
+ * When true, this method may have to initialize the
+ * frame, for example by filling or decoding the prior
+ * frame.
+ * @param opts Options for decoding. May be different from
+ * this->options() for decoding prior frames. Specifies
+ * the frame to decode and whether the prior frame has
+ * already been decoded to fDst. If not, and the frame
+ * is not independent, this method will recursively
+ * decode the frame it depends on.
+ * @param rowsDecoded Out-parameter to report the total number of rows
+ * that have been decoded (or at least written to, if
+ * it had to fill), including rows decoded by prior
+ * calls to onIncrementalDecode.
+ * @return kSuccess if the frame is complete, kIncompleteInput
+ * otherwise.
*/
- static void CloseGif(GifFileType* gif);
-
- /*
- * Frees any extension data used in the decode
- * Used in a SkAutoTCallVProc
- */
- static void FreeExtension(SavedImage* image);
+ Result decodeFrame(bool firstAttempt, const Options& opts, int* rowsDecoded);
/*
* Creates an instance of the decoder
* Called only by NewFromStream
- *
- * @param info contains properties of the encoded data
- * @param stream the stream of image data
- * @param gif pointer to library type that manages gif decode
- * takes ownership
- * @param transIndex The transparent index. An invalid value
- * indicates that there is no transparent index.
+ * Takes ownership of the GIFImageReader
*/
- SkGifCodec(int width, int height, const SkEncodedInfo& info, SkStream* stream,
- GifFileType* gif, uint32_t transIndex, const SkIRect& frameRect, bool frameIsSubset);
-
- SkAutoTCallVProc<GifFileType, CloseGif> fGif; // owned
- SkAutoTDeleteArray<uint8_t> fSrcBuffer;
- const SkIRect fFrameRect;
- const uint32_t fTransIndex;
- uint32_t fFillIndex;
- const bool fFrameIsSubset;
- SkAutoTDelete<SkSwizzler> fSwizzler;
- SkAutoTUnref<SkColorTable> fColorTable;
+ SkGifCodec(const SkEncodedInfo&, const SkImageInfo&, GIFImageReader*);
+
+ std::unique_ptr<GIFImageReader> fReader;
+ std::unique_ptr<uint8_t[]> fTmpBuffer;
+ std::unique_ptr<SkSwizzler> fSwizzler;
+ sk_sp<SkColorTable> fCurrColorTable;
+ // We may create a dummy table if there is not a Map in the input data. In
+ // that case, we set this value to false, and we can skip a lot of decoding
+ // work (which would not be meaningful anyway). We create a "fake"/"dummy"
+ // one in that case, so the client and the swizzler have something to draw.
+ bool fCurrColorTableIsReal;
+ // Whether the background was filled.
+ bool fFilledBackground;
+ // True on the first call to onIncrementalDecode. This value is passed to
+ // decodeFrame.
+ bool fFirstCallToIncrementalDecode;
+
+ void* fDst;
+ size_t fDstRowBytes;
+
+ // Updated inside haveDecodedRow when rows are decoded, unless we filled
+ // the background, in which case it is set once and left alone.
+ int fRowsDecoded;
typedef SkCodec INHERITED;
};
}
return SkCodec::kSuccess;
}
- case SkCodec::kOutOfOrder_SkScanlineOrder:
case SkCodec::kBottomUp_SkScanlineOrder: {
// Note that these modes do not support subsetting.
SkASSERT(0 == subsetY && nativeSize.height() == subsetHeight);
--- /dev/null
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkStreamBuffer.h"
+
+SkStreamBuffer::SkStreamBuffer(SkStream* stream)
+ : fStream(stream)
+ , fBytesBuffered(0)
+{}
+
+size_t SkStreamBuffer::buffer(size_t bytesToBuffer) {
+ // FIXME (scroggo): What should we do if the client tries to read too much?
+ // Should not be a problem in GIF.
+ SkASSERT(fBytesBuffered + bytesToBuffer <= kMaxSize);
+
+ const size_t bytesBuffered = fStream->read(fBuffer + fBytesBuffered, bytesToBuffer);
+ fBytesBuffered += bytesBuffered;
+ return bytesBuffered;
+}
--- /dev/null
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkStreamBuffer_DEFINED
+#define SkStreamBuffer_DEFINED
+
+#include "SkStream.h"
+#include "SkTypes.h"
+
+/**
+ * Helper class for reading from a stream that may not have all its data
+ * available yet.
+ *
+ * Used by GIFImageReader, and currently set up for that use case.
+ *
+ * Buffers up to 256 * 3 bytes (256 colors, with 3 bytes each) to support GIF.
+ * FIXME (scroggo): Make this more general purpose?
+ */
+class SkStreamBuffer : SkNoncopyable {
+public:
+ // Takes ownership of the SkStream.
+ SkStreamBuffer(SkStream*);
+
+ /**
+ * Return a pointer the buffered data.
+ *
+ * The number of bytes buffered is the sum of values returned by calls to
+ * buffer() since the last call to flush().
+ */
+ const char* get() const { SkASSERT(fBytesBuffered >= 1); return fBuffer; }
+
+ /**
+ * Bytes in the buffer.
+ *
+ * Sum of the values returned by calls to buffer() since the last call to
+ * flush().
+ */
+ size_t bytesBuffered() const { return fBytesBuffered; }
+
+ /**
+ * Buffer from the stream into our buffer.
+ *
+ * Returns the number of bytes successfully buffered.
+ */
+ size_t buffer(size_t bytes);
+
+ /**
+ * Flush the buffer.
+ *
+ * After this call, no bytes are buffered.
+ */
+ void flush() {
+ fBytesBuffered = 0;
+ }
+
+private:
+ static constexpr size_t kMaxSize = 256 * 3;
+
+ std::unique_ptr<SkStream> fStream;
+ char fBuffer[kMaxSize];
+ size_t fBytesBuffered;
+};
+#endif // SkStreamBuffer_DEFINED
+
*/
int swizzleWidth() const { return fSwizzleWidth; }
+ /**
+ * Returns the byte offset at which we write to destination memory, taking
+ * scaling, subsetting, and partial frames into account.
+ */
+ size_t swizzleOffsetBytes() const { return fDstOffsetBytes; }
+
private:
/**
--- /dev/null
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkCodec.h"
+#include "SkStream.h"
+
+#include "Resources.h"
+#include "Test.h"
+
+#include <initializer_list>
+#include <vector>
+
+DEF_TEST(Codec_frames, r) {
+ static const struct {
+ const char* fName;
+ size_t fFrameCount;
+ // One less than fFramecount, since the first frame is always
+ // independent.
+ std::vector<size_t> fRequiredFrames;
+ // The size of this one should match fFrameCount for animated, empty
+ // otherwise.
+ std::vector<size_t> fDurations;
+ } gRecs[] = {
+ { "box.gif", 1, {}, {} },
+ { "color_wheel.gif", 1, {}, {} },
+ { "test640x479.gif", 4, { 0, 1, 2 }, { 200, 200, 200, 200 } },
+
+ { "arrow.png", 1, {}, {} },
+ { "google_chrome.ico", 1, {}, {} },
+ { "brickwork-texture.jpg", 1, {}, {} },
+#if defined(SK_CODEC_DECODES_RAW) && (!defined(_WIN32))
+ { "dng_with_preview.dng", 1, {}, {} },
+#endif
+ { "mandrill.wbmp", 1, {}, {} },
+ { "randPixels.bmp", 1, {}, {} },
+ { "yellow_rose.webp", 1, {}, {} },
+ };
+
+ for (auto rec : gRecs) {
+ std::unique_ptr<SkStream> stream(GetResourceAsStream(rec.fName));
+ if (!stream) {
+ // Useful error statement, but sometimes people run tests without
+ // resources, and they do not want to see these messages.
+ //ERRORF(r, "Missing resources? Could not find '%s'", rec.fName);
+ continue;
+ }
+
+ std::unique_ptr<SkCodec> codec(SkCodec::NewFromStream(stream.release()));
+ if (!codec) {
+ ERRORF(r, "Failed to create an SkCodec from '%s'", rec.fName);
+ continue;
+ }
+
+ const size_t expected = rec.fFrameCount;
+ const auto frameInfos = codec->getFrameInfo();
+ // getFrameInfo returns empty set for non-animated.
+ const size_t frameCount = frameInfos.size() == 0 ? 1 : frameInfos.size();
+ if (frameCount != expected) {
+ ERRORF(r, "'%s' expected frame count: %i\tactual: %i", rec.fName, expected, frameCount);
+ continue;
+ }
+
+ if (rec.fRequiredFrames.size() + 1 != expected) {
+ ERRORF(r, "'%s' has wrong number entries in fRequiredFrames; expected: %i\tactual: %i",
+ rec.fName, expected, rec.fRequiredFrames.size());
+ continue;
+ }
+
+ if (1 == frameCount) {
+ continue;
+ }
+
+ // From here on, we are only concerned with animated images.
+ REPORTER_ASSERT(r, frameInfos[0].fRequiredFrame == SkCodec::kNone);
+ for (size_t i = 1; i < frameCount; i++) {
+ REPORTER_ASSERT(r, rec.fRequiredFrames[i-1] == frameInfos[i].fRequiredFrame);
+ }
+
+ // Compare decoding in two ways:
+ // 1. Provide the frame that a frame depends on, so the codec just has to blend.
+ // (in the array cachedFrames)
+ // 2. Do not provide the frame that a frame depends on, so the codec has to decode all the
+ // way back to a key-frame. (in a local variable uncachedFrame)
+ // The two should look the same.
+ std::vector<SkBitmap> cachedFrames(frameCount);
+ const auto& info = codec->getInfo().makeColorType(kN32_SkColorType);
+
+ auto decode = [&](SkBitmap* bm, bool cached, size_t index) {
+ bm->allocPixels(info);
+ if (cached) {
+ // First copy the pixels from the cached frame
+ const size_t requiredFrame = frameInfos[index].fRequiredFrame;
+ if (requiredFrame != SkCodec::kNone) {
+ const bool success = cachedFrames[requiredFrame].copyTo(bm);
+ REPORTER_ASSERT(r, success);
+ }
+ }
+ SkCodec::Options opts;
+ opts.fFrameIndex = index;
+ opts.fHasPriorFrame = cached;
+ const SkCodec::Result result = codec->getPixels(info, bm->getPixels(), bm->rowBytes(),
+ &opts, nullptr, nullptr);
+ REPORTER_ASSERT(r, result == SkCodec::kSuccess);
+ };
+
+ for (size_t i = 0; i < frameCount; i++) {
+ SkBitmap& cachedFrame = cachedFrames[i];
+ decode(&cachedFrame, true, i);
+ SkBitmap uncachedFrame;
+ decode(&uncachedFrame, false, i);
+
+ // Now verify they're equal.
+ const size_t rowLen = info.bytesPerPixel() * info.width();
+ for (int y = 0; y < info.height(); y++) {
+ const void* cachedAddr = cachedFrame.getAddr(0, y);
+ SkASSERT(cachedAddr != nullptr);
+ const void* uncachedAddr = uncachedFrame.getAddr(0, y);
+ SkASSERT(uncachedAddr != nullptr);
+ const bool lineMatches = memcmp(cachedAddr, uncachedAddr, rowLen) == 0;
+ if (!lineMatches) {
+ ERRORF(r, "%s's frame %i is different depending on caching!", rec.fName, i);
+ break;
+ }
+ }
+ }
+
+ if (rec.fDurations.size() != expected) {
+ ERRORF(r, "'%s' has wrong number entries in fDurations; expected: %i\tactual: %i",
+ rec.fName, expected, rec.fDurations.size());
+ continue;
+ }
+
+ for (size_t i = 0; i < frameCount; i++) {
+ REPORTER_ASSERT(r, rec.fDurations[i] == frameInfos[i].fDuration);
+ }
+ }
+}
return SkCodec::kSuccess == codec->getPixels(info, dst->getPixels(), dst->rowBytes());
}
+static void compare_bitmaps(skiatest::Reporter* r, const SkBitmap& bm1, const SkBitmap& bm2) {
+ const SkImageInfo& info = bm1.info();
+ if (info != bm2.info()) {
+ ERRORF(r, "Bitmaps have different image infos!");
+ return;
+ }
+ const size_t rowBytes = info.minRowBytes();
+ for (int i = 0; i < info.height(); i++) {
+ REPORTER_ASSERT(r, !memcmp(bm1.getAddr(0, 0), bm2.getAddr(0, 0), rowBytes));
+ }
+}
+
/*
* Represents a stream without all of its data.
*/
class HaltingStream : public SkStream {
public:
- HaltingStream(sk_sp<SkData> data)
+ HaltingStream(sk_sp<SkData> data, size_t initialLimit)
: fTotalSize(data->size())
- , fLimit(fTotalSize / 2)
+ , fLimit(initialLimit)
, fStream(std::move(data))
{}
- void addNewData() {
- // Arbitrary size, but deliberately different from
- // the buffer size used by SkPngCodec.
- fLimit = SkTMin(fTotalSize, fLimit + 1000);
+ void addNewData(size_t extra) {
+ fLimit = SkTMin(fTotalSize, fLimit + extra);
}
size_t read(void* buffer, size_t size) override {
bool rewind() override { return fStream.rewind(); }
bool move(long offset) override { return fStream.move(offset); }
+ bool isAllDataReceived() const { return fLimit == fTotalSize; }
+
private:
const size_t fTotalSize;
size_t fLimit;
return;
}
- const size_t fileSize = file->size();
-
// Now decode part of the file
- HaltingStream* stream = new HaltingStream(file);
+ HaltingStream* stream = new HaltingStream(file, file->size() / 2);
// Note that we cheat and hold on to a pointer to stream, though it is owned by
// partialCodec.
SkBitmap incremental;
incremental.allocPixels(info);
- const SkCodec::Result startResult = partialCodec->startIncrementalDecode(info,
- incremental.getPixels(), incremental.rowBytes());
- if (startResult != SkCodec::kSuccess) {
- ERRORF(r, "Failed to start incremental decode\n");
- return;
+ while (true) {
+ const SkCodec::Result startResult = partialCodec->startIncrementalDecode(info,
+ incremental.getPixels(), incremental.rowBytes());
+ if (startResult == SkCodec::kSuccess) {
+ break;
+ }
+
+ if (stream->isAllDataReceived()) {
+ ERRORF(r, "Failed to start incremental decode\n");
+ return;
+ }
+
+ // Append some data. The size is arbitrary, but deliberately different from
+ // the buffer size used by SkPngCodec.
+ stream->addNewData(1000);
}
while (true) {
const SkCodec::Result result = partialCodec->incrementalDecode();
- if (stream->getPosition() == fileSize) {
- REPORTER_ASSERT(r, result == SkCodec::kSuccess);
+ if (result == SkCodec::kSuccess) {
break;
}
- SkASSERT(stream->getPosition() < fileSize);
-
REPORTER_ASSERT(r, result == SkCodec::kIncompleteInput);
- // Append an arbitrary amount of data.
- stream->addNewData();
+ if (stream->isAllDataReceived()) {
+ ERRORF(r, "Failed to completely decode %s", name);
+ return;
+ }
+
+ // Append some data. The size is arbitrary, but deliberately different from
+ // the buffer size used by SkPngCodec.
+ stream->addNewData(1000);
}
// compare to original
- for (int i = 0; i < info.height(); i++) {
- REPORTER_ASSERT(r, !memcmp(truth.getAddr(0, 0), incremental.getAddr(0, 0),
- info.minRowBytes()));
- }
+ compare_bitmaps(r, truth, incremental);
}
DEF_TEST(Codec_partial, r) {
test_partial(r, "arrow.png");
test_partial(r, "randPixels.png");
test_partial(r, "baby_tux.png");
+
+ test_partial(r, "box.gif");
+ test_partial(r, "randPixels.gif");
+ test_partial(r, "color_wheel.gif");
+}
+
+DEF_TEST(Codec_partialAnim, r) {
+ auto path = "test640x479.gif";
+ sk_sp<SkData> file = make_from_resource(path);
+ if (!file) {
+ return;
+ }
+
+ // This stream will be owned by fullCodec, but we hang on to the pointer
+ // to determine frame offsets.
+ SkStream* stream = new SkMemoryStream(file);
+ std::unique_ptr<SkCodec> fullCodec(SkCodec::NewFromStream(stream));
+ const auto info = standardize_info(fullCodec.get());
+
+ // frameByteCounts stores the number of bytes to decode a particular frame.
+ // - [0] is the number of bytes for the header
+ // - frames[i] requires frameByteCounts[i+1] bytes to decode
+ std::vector<size_t> frameByteCounts;
+ std::vector<SkBitmap> frames;
+ size_t lastOffset = 0;
+ for (size_t i = 0; true; i++) {
+ frameByteCounts.push_back(stream->getPosition() - lastOffset);
+ lastOffset = stream->getPosition();
+
+ SkBitmap frame;
+ frame.allocPixels(info);
+
+ SkCodec::Options opts;
+ opts.fFrameIndex = i;
+ const SkCodec::Result result = fullCodec->getPixels(info, frame.getPixels(),
+ frame.rowBytes(), &opts, nullptr, nullptr);
+
+ if (result == SkCodec::kIncompleteInput) {
+ frameByteCounts.push_back(stream->getPosition() - lastOffset);
+
+ // We need to distinguish between a partial frame and no more frames.
+ // getFrameInfo lets us do this, since it tells the number of frames
+ // not considering whether they are complete.
+ // FIXME: Should we use a different Result?
+ if (fullCodec->getFrameInfo().size() > i) {
+ // This is a partial frame.
+ frames.push_back(frame);
+ }
+ break;
+ }
+
+ if (result != SkCodec::kSuccess) {
+ ERRORF(r, "Failed to decode frame %i from %s", i, path);
+ return;
+ }
+
+ frames.push_back(frame);
+ }
+
+ // Now decode frames partially, then completely, and compare to the original.
+ HaltingStream* haltingStream = new HaltingStream(file, frameByteCounts[0]);
+ std::unique_ptr<SkCodec> partialCodec(SkCodec::NewFromStream(haltingStream));
+ if (!partialCodec) {
+ ERRORF(r, "Failed to create a partial codec from %s with %i bytes out of %i",
+ path, frameByteCounts[0], file->size());
+ return;
+ }
+
+ SkASSERT(frameByteCounts.size() > frames.size());
+ for (size_t i = 0; i < frames.size(); i++) {
+ const size_t fullFrameBytes = frameByteCounts[i + 1];
+ const size_t firstHalf = fullFrameBytes / 2;
+ const size_t secondHalf = fullFrameBytes - firstHalf;
+
+ haltingStream->addNewData(firstHalf);
+
+ SkBitmap frame;
+ frame.allocPixels(info);
+
+ SkCodec::Options opts;
+ opts.fFrameIndex = i;
+ SkCodec::Result result = partialCodec->startIncrementalDecode(info,
+ frame.getPixels(), frame.rowBytes(), &opts);
+ if (result != SkCodec::kSuccess) {
+ ERRORF(r, "Failed to start incremental decode for %s on frame %i",
+ path, i);
+ return;
+ }
+
+ result = partialCodec->incrementalDecode();
+ REPORTER_ASSERT(r, SkCodec::kIncompleteInput == result);
+
+ haltingStream->addNewData(secondHalf);
+ result = partialCodec->incrementalDecode();
+ REPORTER_ASSERT(r, SkCodec::kSuccess == result);
+
+ // allocPixels locked the pixels for frame, but frames[i] was copied
+ // from another bitmap, and did not retain the locked status.
+ SkAutoLockPixels alp(frames[i]);
+ compare_bitmaps(r, frames[i], frame);
+ }
}
// Test that calling getPixels when an incremental decode has been
// require a call to startIncrementalDecode.
static void test_interleaved(skiatest::Reporter* r, const char* name) {
sk_sp<SkData> file = make_from_resource(name);
+ if (!file) {
+ return;
+ }
+ const size_t halfSize = file->size() / 2;
SkAutoTDelete<SkCodec> partialCodec(SkCodec::NewFromStream(
- new HaltingStream(std::move(file))));
+ new HaltingStream(std::move(file), halfSize)));
if (!partialCodec) {
ERRORF(r, "Failed to create codec for %s", name);
return;
DEF_TEST(Codec_rewind, r) {
test_interleaved(r, "plane.png");
test_interleaved(r, "plane_interlaced.png");
+ test_interleaved(r, "box.gif");
}
if (supportsNewScanlineDecoding && !isIncomplete) {
test_incremental_decode(r, codec, info, codecDigest);
- test_in_stripes(r, codec, info, codecDigest);
+ // This is only supported by codecs that use incremental decoding to
+ // support subset decodes - png and jpeg (once SkJpegCodec is
+ // converted).
+ if (SkStrEndsWith(path, "png") || SkStrEndsWith(path, "PNG")) {
+ test_in_stripes(r, codec, info, codecDigest);
+ }
}
// Need to call startScanlineDecode() first.
check(r, "google_chrome.ico", SkISize::Make(256, 256), false, false, false, true);
// GIF
- // FIXME: We are not ready to test incomplete GIFs
- check(r, "box.gif", SkISize::Make(200, 55), true, false, false);
- check(r, "color_wheel.gif", SkISize::Make(128, 128), true, false, false);
+ check(r, "box.gif", SkISize::Make(200, 55), false, false, true, true);
+ check(r, "color_wheel.gif", SkISize::Make(128, 128), false, false, true, true);
// randPixels.gif is too small to test incomplete
- check(r, "randPixels.gif", SkISize::Make(8, 8), true, false, false);
+ check(r, "randPixels.gif", SkISize::Make(8, 8), false, false, false, true);
// JPG
check(r, "CMYK.jpg", SkISize::Make(642, 516), true, false, true);
test_conversion_possible(r, "yellow_rose.png", false, true);
}
+static void decode_frame(skiatest::Reporter* r, SkCodec* codec, size_t frame) {
+ SkBitmap bm;
+ auto info = codec->getInfo().makeColorType(kN32_SkColorType);
+ bm.allocPixels(info);
+
+ SkCodec::Options opts;
+ opts.fFrameIndex = frame;
+ REPORTER_ASSERT(r, SkCodec::kSuccess == codec->getPixels(info,
+ bm.getPixels(), bm.rowBytes(), &opts, nullptr, nullptr));
+}
+
+// For an animated image, we should only read enough to decode the requested
+// frame if the client never calls getFrameInfo.
+DEF_TEST(Codec_skipFullParse, r) {
+ auto path = "test640x479.gif";
+ SkStream* stream(GetResourceAsStream(path));
+ if (!stream) {
+ return;
+ }
+
+ // Note that we cheat and hold on to the stream pointer, but SkCodec will
+ // take ownership. We will not refer to the stream after the SkCodec
+ // deletes it.
+ std::unique_ptr<SkCodec> codec(SkCodec::NewFromStream(stream));
+ if (!codec) {
+ ERRORF(r, "Failed to create codec for %s", path);
+ return;
+ }
+
+ REPORTER_ASSERT(r, stream->hasPosition());
+ const size_t sizePosition = stream->getPosition();
+ REPORTER_ASSERT(r, stream->hasLength() && sizePosition < stream->getLength());
+
+ // This should read more of the stream, but not the whole stream.
+ decode_frame(r, codec.get(), 0);
+ const size_t positionAfterFirstFrame = stream->getPosition();
+ REPORTER_ASSERT(r, positionAfterFirstFrame > sizePosition
+ && positionAfterFirstFrame < stream->getLength());
+
+ // Again, this should read more of the stream.
+ decode_frame(r, codec.get(), 2);
+ const size_t positionAfterThirdFrame = stream->getPosition();
+ REPORTER_ASSERT(r, positionAfterThirdFrame > positionAfterFirstFrame
+ && positionAfterThirdFrame < stream->getLength());
+
+ // This does not need to read any more of the stream, since it has already
+ // parsed the second frame.
+ decode_frame(r, codec.get(), 1);
+ REPORTER_ASSERT(r, stream->getPosition() == positionAfterThirdFrame);
+
+ // This should read the rest of the frames.
+ decode_frame(r, codec.get(), 3);
+ const size_t finalPosition = stream->getPosition();
+ REPORTER_ASSERT(r, finalPosition > positionAfterThirdFrame);
+
+ // There may be more data in the stream.
+ auto frameInfo = codec->getFrameInfo();
+ REPORTER_ASSERT(r, frameInfo.size() == 4);
+ REPORTER_ASSERT(r, stream->getPosition() >= finalPosition);
+}
+
// Only rewinds up to a limit.
class LimitedRewindingStream : public SkStream {
public:
// Formats that currently do not support incremental decoding
auto files = {
- "box.gif",
"CMYK.jpg",
"color_wheel.ico",
"mandrill.wbmp",
REPORTER_ASSERT(r, bm.height() == 1);
REPORTER_ASSERT(r, !(bm.empty()));
if (!(bm.empty())) {
- REPORTER_ASSERT(r, bm.getColor(0, 0) == 0xFF000000);
+ REPORTER_ASSERT(r, bm.getColor(0, 0) == 0x00000000);
}
}
static void test_gif_data(skiatest::Reporter* r, void* data, size_t size) {
--- /dev/null
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Chris Saari <saari@netscape.com>
+ * Apple Computer
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+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 "GIFImageReader.h"
+#include "SkColorPriv.h"
+#include "SkGifCodec.h"
+
+#include <algorithm>
+#include <string.h>
+
+
+// GETN(n, s) requests at least 'n' bytes available from 'q', at start of state 's'.
+//
+// Note, the hold will never need to be bigger than 256 bytes to gather up in the hold,
+// as each GIF block (except colormaps) can never be bigger than 256 bytes.
+// Colormaps are directly copied in the resp. global_colormap or dynamically allocated local_colormap.
+// So a fixed buffer in GIFImageReader is good enough.
+// This buffer is only needed to copy left-over data from one GifWrite call to the next
+#define GETN(n, s) \
+ do { \
+ m_bytesToConsume = (n); \
+ m_state = (s); \
+ } while (0)
+
+// Get a 16-bit value stored in little-endian format.
+#define GETINT16(p) ((p)[1]<<8|(p)[0])
+
+// Send the data to the display front-end.
+bool GIFLZWContext::outputRow(const unsigned char* rowBegin)
+{
+ int drowStart = irow;
+ int drowEnd = irow;
+
+ // Haeberli-inspired hack for interlaced GIFs: Replicate lines while
+ // displaying to diminish the "venetian-blind" effect as the image is
+ // loaded. Adjust pixel vertical positions to avoid the appearance of the
+ // image crawling up the screen as successive passes are drawn.
+ if (m_frameContext->progressiveDisplay() && m_frameContext->interlaced() && ipass < 4) {
+ unsigned rowDup = 0;
+ unsigned rowShift = 0;
+
+ switch (ipass) {
+ case 1:
+ rowDup = 7;
+ rowShift = 3;
+ break;
+ case 2:
+ rowDup = 3;
+ rowShift = 1;
+ break;
+ case 3:
+ rowDup = 1;
+ rowShift = 0;
+ break;
+ default:
+ break;
+ }
+
+ drowStart -= rowShift;
+ drowEnd = drowStart + rowDup;
+
+ // Extend if bottom edge isn't covered because of the shift upward.
+ if (((m_frameContext->height() - 1) - drowEnd) <= rowShift)
+ drowEnd = m_frameContext->height() - 1;
+
+ // Clamp first and last rows to upper and lower edge of image.
+ if (drowStart < 0)
+ drowStart = 0;
+
+ if ((unsigned)drowEnd >= m_frameContext->height())
+ drowEnd = m_frameContext->height() - 1;
+ }
+
+ // Protect against too much image data.
+ if ((unsigned)drowStart >= m_frameContext->height())
+ return true;
+
+ // CALLBACK: Let the client know we have decoded a row.
+ if (!m_client->haveDecodedRow(m_frameContext->frameId(), rowBegin,
+ drowStart, drowEnd - drowStart + 1, m_frameContext->progressiveDisplay() && m_frameContext->interlaced() && ipass > 1))
+ return false;
+
+ if (!m_frameContext->interlaced())
+ irow++;
+ else {
+ do {
+ switch (ipass) {
+ case 1:
+ irow += 8;
+ if (irow >= m_frameContext->height()) {
+ ipass++;
+ irow = 4;
+ }
+ break;
+
+ case 2:
+ irow += 8;
+ if (irow >= m_frameContext->height()) {
+ ipass++;
+ irow = 2;
+ }
+ break;
+
+ case 3:
+ irow += 4;
+ if (irow >= m_frameContext->height()) {
+ ipass++;
+ irow = 1;
+ }
+ break;
+
+ case 4:
+ irow += 2;
+ if (irow >= m_frameContext->height()) {
+ ipass++;
+ irow = 0;
+ }
+ break;
+
+ default:
+ break;
+ }
+ } while (irow > (m_frameContext->height() - 1));
+ }
+ return true;
+}
+
+// Perform Lempel-Ziv-Welch decoding.
+// Returns true if decoding was successful. In this case the block will have been completely consumed and/or rowsRemaining will be 0.
+// Otherwise, decoding failed; returns false in this case, which will always cause the GIFImageReader to set the "decode failed" flag.
+bool GIFLZWContext::doLZW(const unsigned char* block, size_t bytesInBlock)
+{
+ const size_t width = m_frameContext->width();
+
+ if (rowIter == rowBuffer.end())
+ return true;
+
+ for (const unsigned char* ch = block; bytesInBlock-- > 0; ch++) {
+ // Feed the next byte into the decoder's 32-bit input buffer.
+ datum += ((int) *ch) << bits;
+ bits += 8;
+
+ // Check for underflow of decoder's 32-bit input buffer.
+ while (bits >= codesize) {
+ // Get the leading variable-length symbol from the data stream.
+ int code = datum & codemask;
+ datum >>= codesize;
+ bits -= codesize;
+
+ // Reset the dictionary to its original state, if requested.
+ if (code == clearCode) {
+ codesize = m_frameContext->dataSize() + 1;
+ codemask = (1 << codesize) - 1;
+ avail = clearCode + 2;
+ oldcode = -1;
+ continue;
+ }
+
+ // Check for explicit end-of-stream code.
+ if (code == (clearCode + 1)) {
+ // end-of-stream should only appear after all image data.
+ if (!rowsRemaining)
+ return true;
+ return false;
+ }
+
+ const int tempCode = code;
+ unsigned short codeLength = 0;
+ if (code < avail) {
+ // This is a pre-existing code, so we already know what it
+ // encodes.
+ codeLength = suffixLength[code];
+ rowIter += codeLength;
+ } else if (code == avail && oldcode != -1) {
+ // This is a new code just being added to the dictionary.
+ // It must encode the contents of the previous code, plus
+ // the first character of the previous code again.
+ codeLength = suffixLength[oldcode] + 1;
+ rowIter += codeLength;
+ *--rowIter = firstchar;
+ code = oldcode;
+ } else {
+ // This is an invalid code. The dictionary is just initialized
+ // and the code is incomplete. We don't know how to handle
+ // this case.
+ return false;
+ }
+
+ while (code >= clearCode) {
+ *--rowIter = suffix[code];
+ code = prefix[code];
+ }
+
+ *--rowIter = firstchar = suffix[code];
+
+ // Define a new codeword in the dictionary as long as we've read
+ // more than one value from the stream.
+ if (avail < MAX_DICTIONARY_ENTRIES && oldcode != -1) {
+ prefix[avail] = oldcode;
+ suffix[avail] = firstchar;
+ suffixLength[avail] = suffixLength[oldcode] + 1;
+ ++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.
+ if (!(avail & codemask) && avail < MAX_DICTIONARY_ENTRIES) {
+ ++codesize;
+ codemask += avail;
+ }
+ }
+ oldcode = tempCode;
+ rowIter += codeLength;
+
+ // Output as many rows as possible.
+ unsigned char* rowBegin = rowBuffer.begin();
+ for (; rowBegin + width <= rowIter; rowBegin += width) {
+ if (!outputRow(rowBegin))
+ return false;
+ rowsRemaining--;
+ if (!rowsRemaining)
+ return true;
+ }
+
+ if (rowBegin != rowBuffer.begin()) {
+ // Move the remaining bytes to the beginning of the buffer.
+ const size_t bytesToCopy = rowIter - rowBegin;
+ memcpy(&rowBuffer.front(), rowBegin, bytesToCopy);
+ rowIter = rowBuffer.begin() + bytesToCopy;
+ }
+ }
+ }
+ return true;
+}
+
+sk_sp<SkColorTable> GIFColorMap::buildTable(SkColorType colorType, size_t transparentPixel) const
+{
+ if (!m_isDefined)
+ return nullptr;
+
+ const PackColorProc proc = choose_pack_color_proc(false, colorType);
+ if (m_table) {
+ if (transparentPixel > (unsigned) m_table->count()
+ || m_table->operator[](transparentPixel) == SK_ColorTRANSPARENT) {
+ if (proc == m_packColorProc) {
+ // This SkColorTable has already been built with the same transparent color and
+ // packing proc. Reuse it.
+ return m_table;
+ }
+ }
+ }
+ m_packColorProc = proc;
+
+ SkASSERT(m_colors <= MAX_COLORS);
+ const uint8_t* srcColormap = m_rawData->bytes();
+ SkPMColor colorStorage[MAX_COLORS];
+ for (size_t i = 0; i < m_colors; i++) {
+ if (i == transparentPixel) {
+ colorStorage[i] = SK_ColorTRANSPARENT;
+ } else {
+ colorStorage[i] = proc(255, srcColormap[0], srcColormap[1], srcColormap[2]);
+ }
+ srcColormap += BYTES_PER_COLORMAP_ENTRY;
+ }
+ for (size_t i = m_colors; i < MAX_COLORS; i++) {
+ colorStorage[i] = SK_ColorTRANSPARENT;
+ }
+ m_table = sk_sp<SkColorTable>(new SkColorTable(colorStorage, MAX_COLORS));
+ return m_table;
+}
+
+sk_sp<SkColorTable> GIFImageReader::getColorTable(SkColorType colorType, size_t index) const {
+ if (index >= m_frames.size()) {
+ return nullptr;
+ }
+
+ const GIFFrameContext* frameContext = m_frames[index].get();
+ const GIFColorMap& localColorMap = frameContext->localColorMap();
+ if (localColorMap.isDefined()) {
+ return localColorMap.buildTable(colorType, frameContext->transparentPixel());
+ }
+ if (m_globalColorMap.isDefined()) {
+ return m_globalColorMap.buildTable(colorType, frameContext->transparentPixel());
+ }
+ return nullptr;
+}
+
+// Perform decoding for this frame. frameComplete will be true if the entire frame is decoded.
+// Returns false if a decoding error occurred. This is a fatal error and causes the GIFImageReader to set the "decode failed" flag.
+// Otherwise, either not enough data is available to decode further than before, or the new data has been decoded successfully; returns true in this case.
+bool GIFFrameContext::decode(SkGifCodec* client, bool* frameComplete)
+{
+ *frameComplete = false;
+ if (!m_lzwContext) {
+ // Wait for more data to properly initialize GIFLZWContext.
+ if (!isDataSizeDefined() || !isHeaderDefined())
+ return true;
+
+ m_lzwContext.reset(new GIFLZWContext(client, this));
+ if (!m_lzwContext->prepareToDecode()) {
+ m_lzwContext.reset();
+ return false;
+ }
+
+ m_currentLzwBlock = 0;
+ }
+
+ // Some bad GIFs have extra blocks beyond the last row, which we don't want to decode.
+ while (m_currentLzwBlock < m_lzwBlocks.size() && m_lzwContext->hasRemainingRows()) {
+ if (!m_lzwContext->doLZW(reinterpret_cast<const unsigned char*>(m_lzwBlocks[m_currentLzwBlock]->data()),
+ m_lzwBlocks[m_currentLzwBlock]->size())) {
+ return false;
+ }
+ ++m_currentLzwBlock;
+ }
+
+ // If this frame is data complete then the previous loop must have completely decoded all LZW blocks.
+ // There will be no more decoding for this frame so it's time to cleanup.
+ if (isComplete()) {
+ *frameComplete = true;
+ m_lzwContext.reset();
+ }
+ return true;
+}
+
+// Decode a frame.
+// This method uses GIFFrameContext:decode() to decode the frame; decoding error is reported to client as a critical failure.
+// Return true if decoding has progressed. Return false if an error has occurred.
+bool GIFImageReader::decode(size_t frameIndex, bool* frameComplete)
+{
+ GIFFrameContext* currentFrame = m_frames[frameIndex].get();
+
+ return currentFrame->decode(m_client, frameComplete);
+}
+
+// Parse incoming GIF data stream into internal data structures.
+// Return true if parsing has progressed or there is not enough data.
+// Return false if a fatal error is encountered.
+bool GIFImageReader::parse(GIFImageReader::GIFParseQuery query)
+{
+ if (m_parseCompleted) {
+ return true;
+ }
+
+ // GIFSizeQuery and GIFFrameCountQuery are negative, so this is only meaningful when >= 0.
+ const int lastFrameToParse = (int) query;
+ if (lastFrameToParse >= 0 && (int) m_frames.size() > lastFrameToParse
+ && m_frames[lastFrameToParse]->isComplete()) {
+ // We have already parsed this frame.
+ return true;
+ }
+
+ while (true) {
+ const size_t bytesBuffered = m_streamBuffer.buffer(m_bytesToConsume);
+ if (bytesBuffered < m_bytesToConsume) {
+ // The stream does not yet have enough data. Mark that we need less next time around,
+ // and return.
+ m_bytesToConsume -= bytesBuffered;
+ return true;
+ }
+
+ switch (m_state) {
+ case GIFLZW:
+ SkASSERT(!m_frames.empty());
+ // FIXME: All this copying might be wasteful for e.g. SkMemoryStream
+ m_frames.back()->addLzwBlock(m_streamBuffer.get(), m_streamBuffer.bytesBuffered());
+ GETN(1, GIFSubBlock);
+ break;
+
+ case GIFLZWStart: {
+ SkASSERT(!m_frames.empty());
+ m_frames.back()->setDataSize(this->getOneByte());
+ GETN(1, GIFSubBlock);
+ break;
+ }
+
+ case GIFType: {
+ const char* currentComponent = m_streamBuffer.get();
+
+ // All GIF files begin with "GIF87a" or "GIF89a".
+ if (!memcmp(currentComponent, "GIF89a", 6))
+ m_version = 89;
+ else if (!memcmp(currentComponent, "GIF87a", 6))
+ m_version = 87;
+ else {
+ // This prevents attempting to continue reading this invalid stream.
+ GETN(0, GIFDone);
+ return false;
+ }
+ GETN(7, GIFGlobalHeader);
+ break;
+ }
+
+ case GIFGlobalHeader: {
+ const unsigned char* currentComponent =
+ reinterpret_cast<const unsigned char*>(m_streamBuffer.get());
+
+ // This is the height and width of the "screen" or frame into which
+ // images are rendered. The individual images can be smaller than
+ // the screen size and located with an origin anywhere within the
+ // screen.
+ // Note that we don't inform the client of the size yet, as it might
+ // change after we read the first frame's image header.
+ m_screenWidth = GETINT16(currentComponent);
+ m_screenHeight = GETINT16(currentComponent + 2);
+
+ const size_t globalColorMapColors = 2 << (currentComponent[4] & 0x07);
+
+ if ((currentComponent[4] & 0x80) && globalColorMapColors > 0) { /* global map */
+ m_globalColorMap.setNumColors(globalColorMapColors);
+ GETN(BYTES_PER_COLORMAP_ENTRY * globalColorMapColors, GIFGlobalColormap);
+ break;
+ }
+
+ GETN(1, GIFImageStart);
+ break;
+ }
+
+ case GIFGlobalColormap: {
+ m_globalColorMap.setRawData(m_streamBuffer.get(), m_streamBuffer.bytesBuffered());
+ GETN(1, GIFImageStart);
+ break;
+ }
+
+ case GIFImageStart: {
+ const char currentComponent = m_streamBuffer.get()[0];
+
+ if (currentComponent == '!') { // extension.
+ GETN(2, GIFExtension);
+ break;
+ }
+
+ if (currentComponent == ',') { // image separator.
+ GETN(9, GIFImageHeader);
+ break;
+ }
+
+ // If we get anything other than ',' (image separator), '!'
+ // (extension), or ';' (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 Mozilla's implementation and
+ // proceed as if the file were correctly terminated, so the
+ // GIF will display.
+ GETN(0, GIFDone);
+ break;
+ }
+
+ case GIFExtension: {
+ const unsigned char* currentComponent =
+ reinterpret_cast<const unsigned char*>(m_streamBuffer.get());
+
+ size_t bytesInBlock = currentComponent[1];
+ GIFState exceptionState = GIFSkipBlock;
+
+ switch (*currentComponent) {
+ case 0xf9:
+ exceptionState = GIFControlExtension;
+ // The GIF spec mandates that the GIFControlExtension 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.
+ bytesInBlock = std::max(bytesInBlock, static_cast<size_t>(4));
+ break;
+
+ // The GIF spec also specifies the lengths of the following two extensions' headers
+ // (as 12 and 11 bytes, respectively). Because we ignore the plain text extension entirely
+ // and sanity-check the actual length of the application extension header before reading it,
+ // we allow GIFs to deviate from these values in either direction. 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.
+ case 0x01:
+ // ignoring plain text extension
+ break;
+
+ case 0xff:
+ exceptionState = GIFApplicationExtension;
+ break;
+
+ case 0xfe:
+ exceptionState = GIFConsumeComment;
+ break;
+ }
+
+ if (bytesInBlock)
+ GETN(bytesInBlock, exceptionState);
+ else
+ GETN(1, GIFImageStart);
+ break;
+ }
+
+ case GIFConsumeBlock: {
+ const unsigned char currentComponent = this->getOneByte();
+ if (!currentComponent)
+ GETN(1, GIFImageStart);
+ else
+ GETN(currentComponent, GIFSkipBlock);
+ break;
+ }
+
+ case GIFSkipBlock: {
+ GETN(1, GIFConsumeBlock);
+ break;
+ }
+
+ case GIFControlExtension: {
+ const unsigned char* currentComponent =
+ reinterpret_cast<const unsigned char*>(m_streamBuffer.get());
+
+ addFrameIfNecessary();
+ GIFFrameContext* currentFrame = m_frames.back().get();
+ if (*currentComponent & 0x1)
+ currentFrame->setTransparentPixel(currentComponent[3]);
+
+ // We ignore the "user input" bit.
+
+ // NOTE: This relies on the values in the FrameDisposalMethod enum
+ // matching those in the GIF spec!
+ int rawDisposalMethod = ((*currentComponent) >> 2) & 0x7;
+ switch (rawDisposalMethod) {
+ case 1:
+ case 2:
+ case 3:
+ currentFrame->setDisposalMethod((SkCodecAnimation::DisposalMethod) rawDisposalMethod);
+ break;
+ case 4:
+ // Some specs say that disposal method 3 is "overwrite previous", others that setting
+ // the third bit of the field (i.e. method 4) is. We map both to the same value.
+ currentFrame->setDisposalMethod(SkCodecAnimation::RestorePrevious_DisposalMethod);
+ break;
+ default:
+ // Other values use the default.
+ currentFrame->setDisposalMethod(SkCodecAnimation::Keep_DisposalMethod);
+ break;
+ }
+ currentFrame->setDelayTime(GETINT16(currentComponent + 1) * 10);
+ GETN(1, GIFConsumeBlock);
+ break;
+ }
+
+ case GIFCommentExtension: {
+ const unsigned char currentComponent = this->getOneByte();
+ if (currentComponent)
+ GETN(currentComponent, GIFConsumeComment);
+ else
+ GETN(1, GIFImageStart);
+ break;
+ }
+
+ case GIFConsumeComment: {
+ GETN(1, GIFCommentExtension);
+ break;
+ }
+
+ case GIFApplicationExtension: {
+ // Check for netscape application extension.
+ if (m_streamBuffer.bytesBuffered() == 11) {
+ const unsigned char* currentComponent =
+ reinterpret_cast<const unsigned char*>(m_streamBuffer.get());
+
+ if (!memcmp(currentComponent, "NETSCAPE2.0", 11) || !memcmp(currentComponent, "ANIMEXTS1.0", 11))
+ GETN(1, GIFNetscapeExtensionBlock);
+ }
+
+ if (m_state != GIFNetscapeExtensionBlock)
+ GETN(1, GIFConsumeBlock);
+ break;
+ }
+
+ // Netscape-specific GIF extension: animation looping.
+ case GIFNetscapeExtensionBlock: {
+ const int currentComponent = this->getOneByte();
+ // GIFConsumeNetscapeExtension always reads 3 bytes from the stream; we should at least wait for this amount.
+ if (currentComponent)
+ GETN(std::max(3, currentComponent), GIFConsumeNetscapeExtension);
+ else
+ GETN(1, GIFImageStart);
+ break;
+ }
+
+ // Parse netscape-specific application extensions
+ case GIFConsumeNetscapeExtension: {
+ const unsigned char* currentComponent =
+ reinterpret_cast<const unsigned char*>(m_streamBuffer.get());
+
+ int netscapeExtension = currentComponent[0] & 7;
+
+ // Loop entire animation specified # of times. Only read the loop count during the first iteration.
+ if (netscapeExtension == 1) {
+ m_loopCount = GETINT16(currentComponent + 1);
+
+ // Zero loop count is infinite animation loop request.
+ if (!m_loopCount)
+ m_loopCount = SkCodecAnimation::kAnimationLoopInfinite;
+
+ GETN(1, GIFNetscapeExtensionBlock);
+ } else if (netscapeExtension == 2) {
+ // Wait for specified # of bytes to enter buffer.
+
+ // Don't do this, this extension doesn't exist (isn't used at all)
+ // and doesn't do anything, as our streaming/buffering takes care of it all...
+ // See: http://semmix.pl/color/exgraf/eeg24.htm
+ GETN(1, GIFNetscapeExtensionBlock);
+ } else {
+ // 0,3-7 are yet to be defined netscape extension codes
+ // This prevents attempting to continue reading this invalid stream.
+ GETN(0, GIFDone);
+ return false;
+ }
+ break;
+ }
+
+ case GIFImageHeader: {
+ unsigned height, width, xOffset, yOffset;
+ const unsigned char* currentComponent =
+ reinterpret_cast<const unsigned char*>(m_streamBuffer.get());
+
+ /* Get image offsets, with respect to the screen origin */
+ xOffset = GETINT16(currentComponent);
+ yOffset = GETINT16(currentComponent + 2);
+
+ /* Get image width and height. */
+ width = GETINT16(currentComponent + 4);
+ height = GETINT16(currentComponent + 6);
+
+ // Some GIF files have frames that don't fit in the specified
+ // overall image size. For the first frame, we can simply enlarge
+ // the image size to allow the frame to be visible. We can't do
+ // this on subsequent frames because the rest of the decoding
+ // infrastructure assumes the image size won't change as we
+ // continue decoding, so any subsequent frames that are even
+ // larger will be cropped.
+ // Luckily, handling just the first frame is sufficient to deal
+ // with most cases, e.g. ones where the image size is erroneously
+ // set to zero, since usually the first frame completely fills
+ // the image.
+ if (currentFrameIsFirstFrame()) {
+ m_screenHeight = std::max(m_screenHeight, yOffset + height);
+ m_screenWidth = std::max(m_screenWidth, xOffset + width);
+ }
+
+ // NOTE: Chromium placed this block after setHeaderDefined, down
+ // below we returned true when asked for the size. So Chromium
+ // created an image which would fail. Is this the correct behavior?
+ // We choose to return false early, so we will not create an
+ // SkCodec.
+
+ // Work around more broken GIF files that have zero image width or
+ // height.
+ if (!height || !width) {
+ height = m_screenHeight;
+ width = m_screenWidth;
+ if (!height || !width) {
+ // This prevents attempting to continue reading this invalid stream.
+ GETN(0, GIFDone);
+ return false;
+ }
+ }
+
+ const bool isLocalColormapDefined = currentComponent[8] & 0x80;
+ // The three low-order bits of currentComponent[8] specify the bits per pixel.
+ const size_t numColors = 2 << (currentComponent[8] & 0x7);
+ if (currentFrameIsFirstFrame()) {
+ bool hasTransparentPixel;
+ if (m_frames.size() == 0) {
+ // We did not see a Graphics Control Extension, so no transparent
+ // pixel was specified.
+ hasTransparentPixel = false;
+ } else {
+ // This means we did see a Graphics Control Extension, which specifies
+ // the transparent pixel
+ const size_t transparentPixel = m_frames[0]->transparentPixel();
+ if (isLocalColormapDefined) {
+ hasTransparentPixel = transparentPixel < numColors;
+ } else {
+ const size_t globalColors = m_globalColorMap.numColors();
+ if (!globalColors) {
+ // No color table for this frame, so the frame is empty.
+ // This is technically different from having a transparent
+ // pixel, but we'll treat it the same - nothing to draw here.
+ hasTransparentPixel = true;
+ } else {
+ hasTransparentPixel = transparentPixel < globalColors;
+ }
+ }
+ }
+
+ if (hasTransparentPixel) {
+ m_firstFrameHasAlpha = true;
+ m_firstFrameSupportsIndex8 = true;
+ } else {
+ const bool frameIsSubset = xOffset > 0 || yOffset > 0
+ || xOffset + width < m_screenWidth
+ || yOffset + height < m_screenHeight;
+ m_firstFrameHasAlpha = frameIsSubset;
+ m_firstFrameSupportsIndex8 = !frameIsSubset;
+ }
+ }
+
+ if (query == GIFSizeQuery) {
+ // The decoder needs to stop, so we return here, before
+ // flushing the buffer. Next time through, we'll be in the same
+ // state, requiring the same amount in the buffer.
+ m_bytesToConsume = 0;
+ return true;
+ }
+
+ addFrameIfNecessary();
+ GIFFrameContext* currentFrame = m_frames.back().get();
+
+ currentFrame->setHeaderDefined();
+
+ currentFrame->setRect(xOffset, yOffset, width, height);
+ currentFrame->setInterlaced(currentComponent[8] & 0x40);
+
+ // Overlaying interlaced, transparent GIFs over
+ // existing image data using the Haeberli display hack
+ // requires saving the underlying image in order to
+ // avoid jaggies at the transparency edges. We are
+ // unprepared to deal with that, so don't display such
+ // images progressively. Which means only the first
+ // frame can be progressively displayed.
+ // FIXME: It is possible that a non-transparent frame
+ // can be interlaced and progressively displayed.
+ currentFrame->setProgressiveDisplay(currentFrameIsFirstFrame());
+
+ if (isLocalColormapDefined) {
+ currentFrame->localColorMap().setNumColors(numColors);
+ GETN(BYTES_PER_COLORMAP_ENTRY * numColors, GIFImageColormap);
+ break;
+ }
+
+ GETN(1, GIFLZWStart);
+ break;
+ }
+
+ case GIFImageColormap: {
+ SkASSERT(!m_frames.empty());
+ m_frames.back()->localColorMap().setRawData(m_streamBuffer.get(), m_streamBuffer.bytesBuffered());
+ GETN(1, GIFLZWStart);
+ break;
+ }
+
+ case GIFSubBlock: {
+ const size_t bytesInBlock = this->getOneByte();
+ if (bytesInBlock)
+ GETN(bytesInBlock, GIFLZW);
+ else {
+ // Finished parsing one frame; Process next frame.
+ SkASSERT(!m_frames.empty());
+ // Note that some broken GIF files do not have enough LZW blocks to fully
+ // decode all rows but we treat it as frame complete.
+ m_frames.back()->setComplete();
+ GETN(1, GIFImageStart);
+ if (lastFrameToParse >= 0 && (int) m_frames.size() > lastFrameToParse) {
+ m_streamBuffer.flush();
+ return true;
+ }
+ }
+ break;
+ }
+
+ case GIFDone: {
+ m_parseCompleted = true;
+ return true;
+ }
+
+ default:
+ // We shouldn't ever get here.
+ // This prevents attempting to continue reading this invalid stream.
+ GETN(0, GIFDone);
+ return false;
+ break;
+ } // switch
+ m_streamBuffer.flush();
+ }
+
+ return true;
+}
+
+void GIFImageReader::addFrameIfNecessary()
+{
+ if (m_frames.empty() || m_frames.back()->isComplete()) {
+ const size_t i = m_frames.size();
+ std::unique_ptr<GIFFrameContext> frame(new GIFFrameContext(i));
+ if (0 == i) {
+ frame->setRequiredFrame(SkCodec::kNone);
+ } else {
+ // FIXME: We could correct these after decoding (i.e. some frames may turn out to be
+ // independent although we did not determine that here).
+ const GIFFrameContext* prevFrameContext = m_frames[i - 1].get();
+ switch (prevFrameContext->getDisposalMethod()) {
+ case SkCodecAnimation::Keep_DisposalMethod:
+ frame->setRequiredFrame(i - 1);
+ break;
+ case SkCodecAnimation::RestorePrevious_DisposalMethod:
+ frame->setRequiredFrame(prevFrameContext->getRequiredFrame());
+ break;
+ case SkCodecAnimation::RestoreBGColor_DisposalMethod:
+ // If the prior frame covers the whole image
+ if (prevFrameContext->frameRect() == SkIRect::MakeWH(m_screenWidth,
+ m_screenHeight)
+ // Or the prior frame was independent
+ || prevFrameContext->getRequiredFrame() == SkCodec::kNone)
+ {
+ // This frame is independent, since we clear everything
+ // prior frame to the BG color
+ frame->setRequiredFrame(SkCodec::kNone);
+ } else {
+ frame->setRequiredFrame(i - 1);
+ }
+ break;
+ }
+ }
+ m_frames.push_back(std::move(frame));
+ }
+}
+
+// FIXME: Move this method to close to doLZW().
+bool GIFLZWContext::prepareToDecode()
+{
+ SkASSERT(m_frameContext->isDataSizeDefined() && m_frameContext->isHeaderDefined());
+
+ // Since we use a codesize of 1 more than the datasize, we need to ensure
+ // that our datasize is strictly less than the MAX_DICTIONARY_ENTRY_BITS.
+ if (m_frameContext->dataSize() >= MAX_DICTIONARY_ENTRY_BITS)
+ return false;
+ clearCode = 1 << m_frameContext->dataSize();
+ avail = clearCode + 2;
+ oldcode = -1;
+ codesize = m_frameContext->dataSize() + 1;
+ codemask = (1 << codesize) - 1;
+ datum = bits = 0;
+ ipass = m_frameContext->interlaced() ? 1 : 0;
+ irow = 0;
+
+ // We want to know the longest sequence encodable by a dictionary with
+ // MAX_DICTIONARY_ENTRIES entries. If we ignore the need to encode the base
+ // values themselves at the beginning of the dictionary, as well as the need
+ // for a clear code or a termination code, we could use every entry to
+ // encode a series of multiple values. If the input value stream looked
+ // like "AAAAA..." (a long string of just one value), the first dictionary
+ // entry would encode AA, the next AAA, the next AAAA, and so forth. Thus
+ // the longest sequence would be MAX_DICTIONARY_ENTRIES + 1 values.
+ //
+ // However, we have to account for reserved entries. The first |datasize|
+ // bits are reserved for the base values, and the next two entries are
+ // reserved for the clear code and termination code. In theory a GIF can
+ // set the datasize to 0, meaning we have just two reserved entries, making
+ // the longest sequence (MAX_DICTIONARY_ENTIRES + 1) - 2 values long. Since
+ // each value is a byte, this is also the number of bytes in the longest
+ // encodable sequence.
+ const size_t maxBytes = MAX_DICTIONARY_ENTRIES - 1;
+
+ // Now allocate the output buffer. We decode directly into this buffer
+ // until we have at least one row worth of data, then call outputRow().
+ // This means worst case we may have (row width - 1) bytes in the buffer
+ // and then decode a sequence |maxBytes| long to append.
+ rowBuffer.reset(m_frameContext->width() - 1 + maxBytes);
+ rowIter = rowBuffer.begin();
+ rowsRemaining = m_frameContext->height();
+
+ // Clearing the whole suffix table lets us be more tolerant of bad data.
+ for (int i = 0; i < clearCode; ++i) {
+ suffix[i] = i;
+ suffixLength[i] = 1;
+ }
+ return true;
+}
+
--- /dev/null
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Communicator client code.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 1998
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef GIFImageReader_h
+#define GIFImageReader_h
+
+// Define ourselves as the clientPtr. Mozilla just hacked their C++ callback class into this old C decoder,
+// so we will too.
+class SkGifCodec;
+
+#include "SkCodec.h"
+#include "SkCodecPriv.h"
+#include "SkCodecAnimation.h"
+#include "SkColorTable.h"
+#include "SkData.h"
+#include "SkImageInfo.h"
+#include "SkStreamBuffer.h"
+#include "../private/SkTArray.h"
+#include <memory>
+#include <vector>
+
+typedef SkTArray<unsigned char, true> GIFRow;
+
+
+#define MAX_DICTIONARY_ENTRY_BITS 12
+#define MAX_DICTIONARY_ENTRIES 4096 // 2^MAX_DICTIONARY_ENTRY_BITS
+#define MAX_COLORS 256
+#define BYTES_PER_COLORMAP_ENTRY 3
+
+constexpr int cLoopCountNotSeen = -2;
+constexpr size_t kNotFound = static_cast<size_t>(-1);
+
+// List of possible parsing states.
+enum GIFState {
+ GIFType,
+ GIFGlobalHeader,
+ GIFGlobalColormap,
+ GIFImageStart,
+ GIFImageHeader,
+ GIFImageColormap,
+ GIFImageBody,
+ GIFLZWStart,
+ GIFLZW,
+ GIFSubBlock,
+ GIFExtension,
+ GIFControlExtension,
+ GIFConsumeBlock,
+ GIFSkipBlock,
+ GIFDone,
+ GIFCommentExtension,
+ GIFApplicationExtension,
+ GIFNetscapeExtensionBlock,
+ GIFConsumeNetscapeExtension,
+ GIFConsumeComment
+};
+
+struct GIFFrameContext;
+
+// LZW decoder state machine.
+class GIFLZWContext final : public SkNoncopyable {
+public:
+ GIFLZWContext(SkGifCodec* client, const GIFFrameContext* frameContext)
+ : codesize(0)
+ , codemask(0)
+ , clearCode(0)
+ , avail(0)
+ , oldcode(0)
+ , firstchar(0)
+ , bits(0)
+ , datum(0)
+ , ipass(0)
+ , irow(0)
+ , rowsRemaining(0)
+ , rowIter(0)
+ , m_client(client)
+ , m_frameContext(frameContext)
+ { }
+
+ bool prepareToDecode();
+ bool outputRow(const unsigned char* rowBegin);
+ bool doLZW(const unsigned char* block, size_t bytesInBlock);
+ bool hasRemainingRows() { return rowsRemaining; }
+
+private:
+ // LZW decoding states and output states.
+ int codesize;
+ int codemask;
+ int clearCode; // Codeword used to trigger dictionary reset.
+ int avail; // Index of next available slot in dictionary.
+ int oldcode;
+ unsigned char firstchar;
+ int bits; // Number of unread bits in "datum".
+ int datum; // 32-bit input buffer.
+ int ipass; // Interlace pass; Ranges 1-4 if interlaced.
+ size_t irow; // Current output row, starting at zero.
+ size_t rowsRemaining; // Rows remaining to be output.
+
+ unsigned short prefix[MAX_DICTIONARY_ENTRIES];
+ unsigned char suffix[MAX_DICTIONARY_ENTRIES];
+ unsigned short suffixLength[MAX_DICTIONARY_ENTRIES];
+ GIFRow rowBuffer; // Single scanline temporary buffer.
+ unsigned char* rowIter;
+
+ SkGifCodec* const m_client;
+ const GIFFrameContext* m_frameContext;
+};
+
+class GIFColorMap final {
+public:
+ GIFColorMap()
+ : m_isDefined(false)
+ , m_colors(0)
+ , m_packColorProc(nullptr)
+ {
+ }
+
+ void setNumColors(size_t colors) {
+ m_colors = colors;
+ }
+
+ size_t numColors() const { return m_colors; }
+
+ void setRawData(const char* data, size_t size)
+ {
+ // FIXME: Can we avoid this copy?
+ m_rawData = SkData::MakeWithCopy(data, size);
+ SkASSERT(m_colors * BYTES_PER_COLORMAP_ENTRY == size);
+ m_isDefined = true;
+ }
+ bool isDefined() const { return m_isDefined; }
+
+ // Build RGBA table using the data stream.
+ sk_sp<SkColorTable> buildTable(SkColorType dstColorType, size_t transparentPixel) const;
+
+private:
+ bool m_isDefined;
+ size_t m_colors;
+ sk_sp<SkData> m_rawData;
+ mutable PackColorProc m_packColorProc;
+ mutable sk_sp<SkColorTable> m_table;
+};
+
+// LocalFrame output state machine.
+struct GIFFrameContext : SkNoncopyable {
+public:
+ GIFFrameContext(int id)
+ : m_frameId(id)
+ , m_xOffset(0)
+ , m_yOffset(0)
+ , m_width(0)
+ , m_height(0)
+ , m_transparentPixel(kNotFound)
+ , m_disposalMethod(SkCodecAnimation::Keep_DisposalMethod)
+ , m_requiredFrame(SkCodec::kNone)
+ , m_dataSize(0)
+ , m_progressiveDisplay(false)
+ , m_interlaced(false)
+ , m_delayTime(0)
+ , m_currentLzwBlock(0)
+ , m_isComplete(false)
+ , m_isHeaderDefined(false)
+ , m_isDataSizeDefined(false)
+ {
+ }
+
+ ~GIFFrameContext()
+ {
+ }
+
+ void addLzwBlock(const void* data, size_t size)
+ {
+ m_lzwBlocks.push_back(SkData::MakeWithCopy(data, size));
+ }
+
+ bool decode(SkGifCodec* client, bool* frameDecoded);
+
+ int frameId() const { return m_frameId; }
+ void setRect(unsigned x, unsigned y, unsigned width, unsigned height)
+ {
+ m_xOffset = x;
+ m_yOffset = y;
+ m_width = width;
+ m_height = height;
+ }
+ SkIRect frameRect() const { return SkIRect::MakeXYWH(m_xOffset, m_yOffset, m_width, m_height); }
+ unsigned xOffset() const { return m_xOffset; }
+ unsigned yOffset() const { return m_yOffset; }
+ unsigned width() const { return m_width; }
+ unsigned height() const { return m_height; }
+ size_t transparentPixel() const { return m_transparentPixel; }
+ void setTransparentPixel(size_t pixel) { m_transparentPixel = pixel; }
+ SkCodecAnimation::DisposalMethod getDisposalMethod() const { return m_disposalMethod; }
+ void setDisposalMethod(SkCodecAnimation::DisposalMethod disposalMethod) { m_disposalMethod = disposalMethod; }
+ size_t getRequiredFrame() const { return m_requiredFrame; }
+ void setRequiredFrame(size_t req) { m_requiredFrame = req; }
+ unsigned delayTime() const { return m_delayTime; }
+ void setDelayTime(unsigned delay) { m_delayTime = delay; }
+ bool isComplete() const { return m_isComplete; }
+ void setComplete() { m_isComplete = true; }
+ bool isHeaderDefined() const { return m_isHeaderDefined; }
+ void setHeaderDefined() { m_isHeaderDefined = true; }
+ bool isDataSizeDefined() const { return m_isDataSizeDefined; }
+ int dataSize() const { return m_dataSize; }
+ void setDataSize(int size)
+ {
+ m_dataSize = size;
+ m_isDataSizeDefined = true;
+ }
+ bool progressiveDisplay() const { return m_progressiveDisplay; }
+ void setProgressiveDisplay(bool progressiveDisplay) { m_progressiveDisplay = progressiveDisplay; }
+ bool interlaced() const { return m_interlaced; }
+ void setInterlaced(bool interlaced) { m_interlaced = interlaced; }
+
+ void clearDecodeState() { m_lzwContext.reset(); }
+ const GIFColorMap& localColorMap() const { return m_localColorMap; }
+ GIFColorMap& localColorMap() { return m_localColorMap; }
+
+private:
+ int m_frameId;
+ unsigned m_xOffset;
+ unsigned m_yOffset; // With respect to "screen" origin.
+ unsigned m_width;
+ unsigned m_height;
+ size_t m_transparentPixel; // Index of transparent pixel. Value is kNotFound if there is no transparent pixel.
+ SkCodecAnimation::DisposalMethod m_disposalMethod; // Restore to background, leave in place, etc.
+ size_t m_requiredFrame;
+ int m_dataSize;
+
+ bool m_progressiveDisplay; // If true, do Haeberli interlace hack.
+ bool m_interlaced; // True, if scanlines arrive interlaced order.
+
+ unsigned m_delayTime; // Display time, in milliseconds, for this image in a multi-image GIF.
+
+ std::unique_ptr<GIFLZWContext> m_lzwContext;
+ std::vector<sk_sp<SkData>> m_lzwBlocks; // LZW blocks for this frame.
+ GIFColorMap m_localColorMap;
+
+ size_t m_currentLzwBlock;
+ bool m_isComplete;
+ bool m_isHeaderDefined;
+ bool m_isDataSizeDefined;
+};
+
+class GIFImageReader final : public SkNoncopyable {
+public:
+ // This takes ownership of stream.
+ GIFImageReader(SkStream* stream)
+ : m_client(nullptr)
+ , m_state(GIFType)
+ , m_bytesToConsume(6) // Number of bytes for GIF type, either "GIF87a" or "GIF89a".
+ , m_version(0)
+ , m_screenWidth(0)
+ , m_screenHeight(0)
+ , m_loopCount(cLoopCountNotSeen)
+ , m_streamBuffer(stream)
+ , m_parseCompleted(false)
+ , m_firstFrameHasAlpha(false)
+ , m_firstFrameSupportsIndex8(false)
+ {
+ }
+
+ ~GIFImageReader()
+ {
+ }
+
+ void setClient(SkGifCodec* client) { m_client = client; }
+
+ unsigned screenWidth() const { return m_screenWidth; }
+ unsigned screenHeight() const { return m_screenHeight; }
+
+ // Option to pass to parse(). All enums are negative, because a non-negative value is used to
+ // indicate that the Reader should parse up to and including the frame indicated.
+ enum GIFParseQuery {
+ // Parse enough to determine the size. Note that this parses the first frame's header,
+ // since we may decide to expand based on the frame's dimensions.
+ GIFSizeQuery = -1,
+ // Parse to the end, so we know about all frames.
+ GIFFrameCountQuery = -2,
+ };
+
+ // Parse incoming GIF data stream into internal data structures.
+ // Non-negative values are used to indicate to parse through that frame.
+ // Return true if parsing has progressed or there is not enough data.
+ // Return false if a fatal error is encountered.
+ bool parse(GIFParseQuery);
+
+ // Decode the frame indicated by frameIndex.
+ // frameComplete will be set to true if the frame is completely decoded.
+ // The method returns false if there is an error.
+ bool decode(size_t frameIndex, bool* frameComplete);
+
+ size_t imagesCount() const
+ {
+ if (m_frames.empty())
+ return 0;
+
+ // This avoids counting an empty frame when the file is truncated right after
+ // GIFControlExtension but before GIFImageHeader.
+ // FIXME: This extra complexity is not necessary and we should just report m_frames.size().
+ return m_frames.back()->isHeaderDefined() ? m_frames.size() : m_frames.size() - 1;
+ }
+ int loopCount() const { return m_loopCount; }
+
+ const GIFColorMap& globalColorMap() const
+ {
+ return m_globalColorMap;
+ }
+
+ const GIFFrameContext* frameContext(size_t index) const
+ {
+ return index < m_frames.size() ? m_frames[index].get() : 0;
+ }
+
+ void clearDecodeState() {
+ for (size_t index = 0; index < m_frames.size(); index++) {
+ m_frames[index]->clearDecodeState();
+ }
+ }
+
+ // Return the color table for frame index (which may be the global color table).
+ sk_sp<SkColorTable> getColorTable(SkColorType dstColorType, size_t index) const;
+
+ bool firstFrameHasAlpha() const { return m_firstFrameHasAlpha; }
+
+ bool firstFrameSupportsIndex8() const { return m_firstFrameSupportsIndex8; }
+
+private:
+ // Requires that one byte has been buffered into m_streamBuffer.
+ unsigned char getOneByte() const {
+ return reinterpret_cast<const unsigned char*>(m_streamBuffer.get())[0];
+ }
+
+ void addFrameIfNecessary();
+ bool currentFrameIsFirstFrame() const
+ {
+ return m_frames.empty() || (m_frames.size() == 1u && !m_frames[0]->isComplete());
+ }
+
+ // Unowned pointer
+ SkGifCodec* m_client;
+
+ // Parsing state machine.
+ GIFState m_state; // Current decoder master state.
+ size_t m_bytesToConsume; // Number of bytes to consume for next stage of parsing.
+
+ // Global (multi-image) state.
+ int m_version; // Either 89 for GIF89 or 87 for GIF87.
+ unsigned m_screenWidth; // Logical screen width & height.
+ unsigned m_screenHeight;
+ GIFColorMap m_globalColorMap;
+ int m_loopCount; // Netscape specific extension block to control the number of animation loops a GIF renders.
+
+ std::vector<std::unique_ptr<GIFFrameContext>> m_frames;
+
+ SkStreamBuffer m_streamBuffer;
+ bool m_parseCompleted;
+
+ // These values can be computed before we create a GIFFrameContext, so we
+ // store them here instead of on m_frames[0].
+ bool m_firstFrameHasAlpha;
+ bool m_firstFrameSupportsIndex8;
+};
+
+#endif