diff options
author | Leon Scroggins III <scroggo@google.com> | 2017-05-23 09:37:21 -0400 |
---|---|---|
committer | Skia Commit-Bot <skia-commit-bot@chromium.org> | 2017-05-23 15:28:37 +0000 |
commit | 557fbbe05ba48bcc20be684d11fe0edfc24c87ba (patch) | |
tree | de83760e6e67a0866578056b4091091cef29cc53 | |
parent | e980174d0f3af8e497d9c38851114cc6a3c02e9a (diff) |
Add animation support to SkWebpCodec
TBR=reed@google.com
(No change to the public API, but changed a header file)
SkWebpCodec:
- Implement onGetFrameCount, onGetFrameInfo, and onGetRepetitionCount
- Respect the alpha reported by libwebp. Although the spec states that
it is only a hint, the libwebp encoder uses it properly. Respecting
allows us to draw opaque images faster and decode them to 565. This
also matches other SkCodecs (and Chromium).
- onGetPixels:
- Decode the frame requested, recursively decoding required frame if
necessary
- When blending with a prior frame, use SkRasterPipeline
SkCodec:
- Move check for negative index to getFrameInfo
- Reset the colorXform if one is not needed
SkCodecAnimation:
- Add new blend enum, for WebP's (and APNG's) non-blending option
SkFrameHolder:
- New base classes for frames and the owner of the frames, allowing
code sharing between SkWebpCodec and SkGifCodec (particularly for
determining whether a frame has alpha and what frame it depends on)
- When moving items from SkGIFFrameContext, use Skia conventions (i.e.
int instead of unsigned)
- Rename "delay time" to "duration", to match e.g. SkFrameInfo::
fDuration
SkGifImageReader:
- Move pieces to SkFrameHolder, and adapt to changes made in the
process
- Make setAlphaAndRequiredFrame (now on the base class SkFrameHolder)
more general to support webp, and add support for frames that do not
blend
- Change SkGIFFrameContext from a struct to a class, to match how we
use the distinction elsewhere (i.e. struct is a small object with
public fields)
- Rework hasTransparentPixel (now hasTransparency, since it returns true
in some cases where there is not a transparent pixel) to better fit
with the modified setAlphaAndRequiredFrame. Also be more consistent
when there is no transparent pixel but no color map.
- Simplify an if condition that was previously simplified in 2d61e717
but accidentally got reverted in a4db9be6
CodecAnimTest:
- Test new animated webp files
- Rearrange the test to more cleanly print alpha type mismatches for
the first frame
resources:
- webp-animated.webp
- animated webp from Chromium
- blendBG.webp
- new webp file using bits of webp-animated-semitransparent4.webp
from Chromium
- tests required frame and alpha when using the non-blending mode
- frames have the following properties:
- Frame 0: no alpha, fills screen
- Frame 1: alpha, fills screen
- Frame 2: no alpha, fills screen
- Frame 3: alpha, fills screen, blendBG
- Frame 4: no alpha, fills screen, blendBG
- Frame 5: alpha, blendBG
- Frame 6: covers 4, has alpha, blendBG
- also used to test decoding to 565 if the new frame data has alpha
but blends onto an opaque frame
DM.cpp:
- Test animated images to non-native 8888 and unpremul
DMSrcSink.cpp:
- Do not test non-native 8888 decodes to f16 dst
- Test unpremul decodes to f16
- Copy a frame of an animated image prior to drawing, since in unpremul
mode, the DM code will premultiply first.
Bug: skia: 3315
Change-Id: I4e55ae2ee5bc095b37a743bdcfac644be603b980
Reviewed-on: https://skia-review.googlesource.com/16707
Commit-Queue: Mike Reed <reed@google.com>
Reviewed-by: Mike Reed <reed@google.com>
Reviewed-by: Leon Scroggins <scroggo@google.com>
Reviewed-by: Matt Sarett <msarett@google.com>
-rw-r--r-- | dm/DM.cpp | 8 | ||||
-rw-r--r-- | dm/DMSrcSink.cpp | 28 | ||||
-rw-r--r-- | include/codec/SkCodec.h | 3 | ||||
-rw-r--r-- | resources/blendBG.webp | bin | 0 -> 776 bytes | |||
-rwxr-xr-x | resources/webp-animated.webp | bin | 0 -> 340 bytes | |||
-rw-r--r-- | src/codec/SkCodec.cpp | 2 | ||||
-rw-r--r-- | src/codec/SkCodecAnimation.h | 19 | ||||
-rw-r--r-- | src/codec/SkFrameHolder.h | 186 | ||||
-rw-r--r-- | src/codec/SkGifCodec.cpp | 5 | ||||
-rw-r--r-- | src/codec/SkWebpCodec.cpp | 419 | ||||
-rw-r--r-- | src/codec/SkWebpCodec.h | 59 | ||||
-rw-r--r-- | tests/CodecAnimTest.cpp | 52 | ||||
-rw-r--r-- | third_party/gif/SkGifImageReader.cpp | 116 | ||||
-rw-r--r-- | third_party/gif/SkGifImageReader.h | 86 |
14 files changed, 772 insertions, 211 deletions
@@ -603,8 +603,12 @@ static void push_codec_srcs(Path path) { { 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); + for (auto dstCT : { CodecSrc::kNonNative8888_Always_DstColorType, + CodecSrc::kGetFromCanvas_DstColorType }) { + for (auto at : { kUnpremul_SkAlphaType, kPremul_SkAlphaType }) { + push_codec_src(path, CodecSrc::kAnimated_Mode, dstCT, at, 1.0f); + } + } } } diff --git a/dm/DMSrcSink.cpp b/dm/DMSrcSink.cpp index 752ab2e4ae..8979d2d285 100644 --- a/dm/DMSrcSink.cpp +++ b/dm/DMSrcSink.cpp @@ -364,7 +364,8 @@ static bool get_decode_info(SkImageInfo* decodeInfo, SkColorType canvasColorType *decodeInfo = decodeInfo->makeColorType(kGray_8_SkColorType); break; case CodecSrc::kNonNative8888_Always_DstColorType: - if (kRGB_565_SkColorType == canvasColorType) { + if (kRGB_565_SkColorType == canvasColorType + || kRGBA_F16_SkColorType == canvasColorType) { return false; } #ifdef SK_PMCOLOR_IS_RGBA @@ -380,11 +381,6 @@ static bool get_decode_info(SkImageInfo* decodeInfo, SkColorType canvasColorType } if (kRGBA_F16_SkColorType == canvasColorType) { - if (kUnpremul_SkAlphaType == dstAlphaType) { - // Testing kPremul is enough for adequate coverage of F16 decoding. - return false; - } - sk_sp<SkColorSpace> linearSpace = as_CSB(decodeInfo->colorSpace())->makeLinearGamma(); *decodeInfo = decodeInfo->makeColorSpace(std::move(linearSpace)); @@ -499,6 +495,18 @@ Error CodecSrc::draw(SkCanvas* canvas) const { switch (result) { case SkCodec::kSuccess: case SkCodec::kIncompleteInput: { + // If the next frame depends on this one, store it in priorFrame. + // It is possible that we may discard a frame that future frames depend on, + // but the codec will simply redecode the discarded frame. + // Do this before calling draw_to_canvas, which premultiplies in place. If + // we're decoding to unpremul, we want to pass the unmodified frame to the + // codec for decoding the next frame. + if (static_cast<size_t>(i+1) < frameInfos.size() + && frameInfos[i+1].fRequiredFrame == i) { + memcpy(priorFramePixels.reset(safeSize), pixels.get(), safeSize); + cachedFrame = i; + } + SkAutoCanvasRestore acr(canvas, true); const int xTranslate = (i % factor) * decodeInfo.width(); const int yTranslate = (i / factor) * decodeInfo.height(); @@ -521,14 +529,6 @@ Error CodecSrc::draw(SkCanvas* canvas) const { 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 (static_cast<size_t>(i+1) < frameInfos.size() - && frameInfos[i+1].fRequiredFrame == i) { - memcpy(priorFramePixels.reset(safeSize), pixels.get(), safeSize); - cachedFrame = i; - } } break; } diff --git a/include/codec/SkCodec.h b/include/codec/SkCodec.h index 7f88fc6117..d52433e3a3 100644 --- a/include/codec/SkCodec.h +++ b/include/codec/SkCodec.h @@ -644,6 +644,9 @@ public: * have not already been parsed. */ bool getFrameInfo(int index, FrameInfo* info) const { + if (index < 0) { + return false; + } return this->onGetFrameInfo(index, info); } diff --git a/resources/blendBG.webp b/resources/blendBG.webp Binary files differnew file mode 100644 index 0000000000..46e4ce255d --- /dev/null +++ b/resources/blendBG.webp diff --git a/resources/webp-animated.webp b/resources/webp-animated.webp Binary files differnew file mode 100755 index 0000000000..35a8dfcf34 --- /dev/null +++ b/resources/webp-animated.webp diff --git a/src/codec/SkCodec.cpp b/src/codec/SkCodec.cpp index 9e693b1dfa..688cfaa993 100644 --- a/src/codec/SkCodec.cpp +++ b/src/codec/SkCodec.cpp @@ -485,6 +485,8 @@ bool SkCodec::initializeColorXform(const SkImageInfo& dstInfo, if (!fColorXform) { return false; } + } else { + fColorXform.reset(); } return true; diff --git a/src/codec/SkCodecAnimation.h b/src/codec/SkCodecAnimation.h index b0495b89ee..46a878a801 100644 --- a/src/codec/SkCodecAnimation.h +++ b/src/codec/SkCodecAnimation.h @@ -44,6 +44,25 @@ public: RestorePrevious_DisposalMethod = 3, }; + /** + * How to blend the current frame. + */ + enum class Blend { + /** + * Blend with the prior frame. This is the typical case, supported + * by all animated image types. + */ + kPriorFrame, + + /** + * Do not blend. + * + * This frames pixels overwrite previous pixels "blending" with + * the background color of transparent. + */ + kBG, + }; + private: SkCodecAnimation(); }; diff --git a/src/codec/SkFrameHolder.h b/src/codec/SkFrameHolder.h new file mode 100644 index 0000000000..e92b40f9d8 --- /dev/null +++ b/src/codec/SkFrameHolder.h @@ -0,0 +1,186 @@ +/* + * Copyright 2017 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkFrameHolder_DEFINED +#define SkFrameHolder_DEFINED + +#include "SkTypes.h" +#include "SkCodecAnimation.h" + +/** + * Base class for a single frame of an animated image. + * + * Separate from SkCodec::FrameInfo, which is a pared down + * interface that only contains the info the client needs. + */ +class SkFrame : public SkNoncopyable { +public: + SkFrame(int id) + : fId(id) + , fHasAlpha(false) + , fRequiredFrame(kUninitialized) + , fDisposalMethod(SkCodecAnimation::Keep_DisposalMethod) + , fDuration(0) + , fBlend(SkCodecAnimation::Blend::kPriorFrame) + { + fRect.setEmpty(); + } + + virtual ~SkFrame() {} + + /** + * 0-based index of the frame in the image sequence. + */ + int frameId() const { return fId; } + + /** + * Whether this frame reports alpha. + * + * This only considers the rectangle of this frame, and + * considers it to have alpha even if it is opaque once + * blended with the frame behind it. + */ + bool reportsAlpha() const { + return this->onReportsAlpha(); + } + + /** + * Cached value representing whether the frame has alpha, + * after compositing with the prior frame. + */ + bool hasAlpha() const { return fHasAlpha; } + + /** + * Cache whether the finished frame has alpha. + */ + void setHasAlpha(bool alpha) { fHasAlpha = alpha; } + + /** + * Whether enough of the frame has been read to determine + * fRequiredFrame and fHasAlpha. + */ + bool reachedStartOfData() const { return fRequiredFrame != kUninitialized; } + + /** + * The frame this one depends on. + * + * Must not be called until fRequiredFrame has been set properly. + */ + int getRequiredFrame() const { + SkASSERT(this->reachedStartOfData()); + return fRequiredFrame; + } + + /** + * Set the frame that this frame depends on. + */ + void setRequiredFrame(int req) { fRequiredFrame = req; } + + /** + * Set the rectangle that is updated by this frame. + */ + void setXYWH(int x, int y, int width, int height) { + fRect.setXYWH(x, y, width, height); + } + + /** + * The rectangle that is updated by this frame. + */ + SkIRect frameRect() const { return fRect; } + + int xOffset() const { return fRect.x(); } + int yOffset() const { return fRect.y(); } + int width() const { return fRect.width(); } + int height() const { return fRect.height(); } + + SkCodecAnimation::DisposalMethod getDisposalMethod() const { + return fDisposalMethod; + } + + void setDisposalMethod(SkCodecAnimation::DisposalMethod disposalMethod) { + fDisposalMethod = disposalMethod; + } + + /** + * Set the duration (in ms) to show this frame. + */ + void setDuration(int duration) { + fDuration = duration; + } + + /** + * Duration in ms to show this frame. + */ + int getDuration() const { + return fDuration; + } + + void setBlend(SkCodecAnimation::Blend blend) { + fBlend = blend; + } + + SkCodecAnimation::Blend getBlend() const { + return fBlend; + } + +protected: + virtual bool onReportsAlpha() const = 0; + +private: + static constexpr int kUninitialized = -2; + + const int fId; + bool fHasAlpha; + int fRequiredFrame; + SkIRect fRect; + SkCodecAnimation::DisposalMethod fDisposalMethod; + int fDuration; + SkCodecAnimation::Blend fBlend; +}; + +/** + * Base class for an object which holds the SkFrames of an + * image sequence. + */ +class SkFrameHolder : public SkNoncopyable { +public: + SkFrameHolder() + : fScreenWidth(0) + , fScreenHeight(0) + {} + + virtual ~SkFrameHolder() {} + + /** + * Size of the image. Each frame will be contained in + * these dimensions (possibly after clipping). + */ + int screenWidth() const { return fScreenWidth; } + int screenHeight() const { return fScreenHeight; } + + /** + * Compute the opacity and required frame, based on + * whether the frame reportsAlpha and how it blends + * with prior frames. + */ + void setAlphaAndRequiredFrame(SkFrame*); + + /** + * Return the frame with frameId i. + */ + const SkFrame* getFrame(int i) const { + return this->onGetFrame(i); + } + +protected: + int fScreenWidth; + int fScreenHeight; + + virtual const SkFrame* onGetFrame(int i) const = 0; +}; + +#endif // SkFrameHolder_DEFINED diff --git a/src/codec/SkGifCodec.cpp b/src/codec/SkGifCodec.cpp index 2584ee57c7..1a01c296fc 100644 --- a/src/codec/SkGifCodec.cpp +++ b/src/codec/SkGifCodec.cpp @@ -148,7 +148,7 @@ bool SkGifCodec::onGetFrameInfo(int i, SkCodec::FrameInfo* frameInfo) const { } if (frameInfo) { - frameInfo->fDuration = frameContext->delayTime(); + frameInfo->fDuration = frameContext->getDuration(); frameInfo->fRequiredFrame = frameContext->getRequiredFrame(); frameInfo->fFullyReceived = frameContext->isComplete(); frameInfo->fAlphaType = frameContext->hasAlpha() ? kUnpremul_SkAlphaType @@ -282,8 +282,7 @@ void SkGifCodec::initializeSwizzler(const SkImageInfo& dstInfo, int frameIndex) SkASSERT(frame); const int xBegin = frame->xOffset(); - const int xEnd = std::min(static_cast<int>(frame->xOffset() + frame->width()), - static_cast<int>(fReader->screenWidth())); + const int xEnd = std::min(frame->frameRect().right(), 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. diff --git a/src/codec/SkWebpCodec.cpp b/src/codec/SkWebpCodec.cpp index ab0b91b112..c99d8a7689 100644 --- a/src/codec/SkWebpCodec.cpp +++ b/src/codec/SkWebpCodec.cpp @@ -5,8 +5,11 @@ * found in the LICENSE file. */ +#include "SkBitmap.h" +#include "SkCanvas.h" #include "SkCodecPriv.h" #include "SkColorSpaceXform.h" +#include "SkRasterPipeline.h" #include "SkSampler.h" #include "SkStreamPriv.h" #include "SkTemplates.h" @@ -32,10 +35,12 @@ bool SkWebpCodec::IsWebp(const void* buf, size_t bytesRead) { return bytesRead >= 14 && !memcmp(bytes, "RIFF", 4) && !memcmp(&bytes[8], "WEBPVP", 6); } +static SkAlphaType alpha_type(bool hasAlpha) { + return hasAlpha ? kUnpremul_SkAlphaType : kOpaque_SkAlphaType; +} + // Parse headers of RIFF container, and check for valid Webp (VP8) content. -// NOTE: This calls peek instead of read, since onGetPixels will need these -// bytes again. -// Returns an SkWebpCodec on success; +// Returns an SkWebpCodec on success SkCodec* SkWebpCodec::NewFromStream(SkStream* stream) { std::unique_ptr<SkStream> streamDeleter(stream); @@ -90,8 +95,6 @@ SkCodec* SkWebpCodec::NewFromStream(SkStream* stream) { } // Get the first frame and its "features" to determine the color and alpha types. - // Since we do not yet support animated webp, this is the only frame that we will - // decode. WebPIterator frame; SkAutoTCallVProc<WebPIterator, WebPDemuxReleaseIterator> autoFrame(&frame); if (!WebPDemuxGetFrame(demux, 1, &frame)) { @@ -104,23 +107,32 @@ SkCodec* SkWebpCodec::NewFromStream(SkStream* stream) { return nullptr; } + const bool hasAlpha = SkToBool(frame.has_alpha) + || frame.width != width || frame.height != height; SkEncodedInfo::Color color; SkEncodedInfo::Alpha alpha; switch (features.format) { case 0: // This indicates a "mixed" format. We could see this for // animated webps (multiple fragments). - // I believe that this is a rare case. // We could also guess kYUV here, but I think it makes more // sense to guess kBGRA which is likely closer to the final // output. Otherwise, we might end up converting // BGRA->YUVA->BGRA. - color = SkEncodedInfo::kBGRA_Color; - alpha = SkEncodedInfo::kUnpremul_Alpha; + // Fallthrough: + case 2: + // This is the lossless format (BGRA). + if (hasAlpha) { + color = SkEncodedInfo::kBGRA_Color; + alpha = SkEncodedInfo::kUnpremul_Alpha; + } else { + color = SkEncodedInfo::kBGRX_Color; + alpha = SkEncodedInfo::kOpaque_Alpha; + } break; case 1: // This is the lossy format (YUV). - if (SkToBool(features.has_alpha) || frame.width != width || frame.height != height) { + if (hasAlpha) { color = SkEncodedInfo::kYUVA_Color; alpha = SkEncodedInfo::kUnpremul_Alpha; } else { @@ -128,11 +140,6 @@ SkCodec* SkWebpCodec::NewFromStream(SkStream* stream) { alpha = SkEncodedInfo::kOpaque_Alpha; } break; - case 2: - // This is the lossless format (BGRA). - color = SkEncodedInfo::kBGRA_Color; - alpha = SkEncodedInfo::kUnpremul_Alpha; - break; default: return nullptr; } @@ -160,8 +167,9 @@ bool SkWebpCodec::onDimensionsSupported(const SkISize& dim) { && dim.height() >= 1 && dim.height() <= info.height(); } -static WEBP_CSP_MODE webp_decode_mode(SkColorType ct, bool premultiply) { - switch (ct) { +static WEBP_CSP_MODE webp_decode_mode(const SkImageInfo& info) { + const bool premultiply = info.alphaType() == kPremul_SkAlphaType; + switch (info.colorType()) { case kBGRA_8888_SkColorType: return premultiply ? MODE_bgrA : MODE_BGRA; case kRGBA_8888_SkColorType: @@ -173,6 +181,12 @@ static WEBP_CSP_MODE webp_decode_mode(SkColorType ct, bool premultiply) { } } +SkWebpCodec::Frame* SkWebpCodec::FrameHolder::appendNewFrame(bool hasAlpha) { + const int i = this->size(); + fFrames.emplace_back(i, hasAlpha); + return &fFrames[i]; +} + bool SkWebpCodec::onGetValidSubset(SkIRect* desiredSubset) const { if (!desiredSubset) { return false; @@ -191,13 +205,210 @@ bool SkWebpCodec::onGetValidSubset(SkIRect* desiredSubset) const { return true; } +int SkWebpCodec::onGetRepetitionCount() { + auto flags = WebPDemuxGetI(fDemux.get(), WEBP_FF_FORMAT_FLAGS); + if (!(flags & ANIMATION_FLAG)) { + return 0; + } + + const int repCount = WebPDemuxGetI(fDemux.get(), WEBP_FF_LOOP_COUNT); + if (0 == repCount) { + return kRepetitionCountInfinite; + } + + return repCount; +} + +int SkWebpCodec::onGetFrameCount() { + auto flags = WebPDemuxGetI(fDemux.get(), WEBP_FF_FORMAT_FLAGS); + if (!(flags & ANIMATION_FLAG)) { + return 1; + } + + const uint32_t oldFrameCount = fFrameHolder.size(); + if (fFailed) { + return oldFrameCount; + } + + const uint32_t frameCount = WebPDemuxGetI(fDemux, WEBP_FF_FRAME_COUNT); + if (oldFrameCount == frameCount) { + // We have already parsed this. + return frameCount; + } + + fFrameHolder.reserve(frameCount); + + for (uint32_t i = oldFrameCount; i < frameCount; i++) { + WebPIterator iter; + SkAutoTCallVProc<WebPIterator, WebPDemuxReleaseIterator> autoIter(&iter); + + if (!WebPDemuxGetFrame(fDemux.get(), i + 1, &iter)) { + fFailed = true; + break; + } + + // libwebp only reports complete frames of an animated image. + SkASSERT(iter.complete); + + Frame* frame = fFrameHolder.appendNewFrame(iter.has_alpha); + frame->setXYWH(iter.x_offset, iter.y_offset, iter.width, iter.height); + frame->setDisposalMethod(iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND ? + SkCodecAnimation::RestoreBGColor_DisposalMethod : + SkCodecAnimation::Keep_DisposalMethod); + frame->setDuration(iter.duration); + if (WEBP_MUX_BLEND != iter.blend_method) { + frame->setBlend(SkCodecAnimation::Blend::kBG); + } + fFrameHolder.setAlphaAndRequiredFrame(frame); + } + + return fFrameHolder.size(); + +} + +const SkFrame* SkWebpCodec::FrameHolder::onGetFrame(int i) const { + return static_cast<const SkFrame*>(this->frame(i)); +} + +const SkWebpCodec::Frame* SkWebpCodec::FrameHolder::frame(int i) const { + SkASSERT(i >= 0 && i < this->size()); + return &fFrames[i]; +} + +bool SkWebpCodec::onGetFrameInfo(int i, FrameInfo* frameInfo) const { + if (i >= fFrameHolder.size()) { + return false; + } + + const Frame* frame = fFrameHolder.frame(i); + if (!frame) { + return false; + } + + if (frameInfo) { + frameInfo->fRequiredFrame = frame->getRequiredFrame(); + frameInfo->fDuration = frame->getDuration(); + // libwebp only reports fully received frames for an + // animated image. + frameInfo->fFullyReceived = true; + frameInfo->fAlphaType = alpha_type(frame->hasAlpha()); + } + + return true; +} + +static bool is_8888(SkColorType colorType) { + switch (colorType) { + case kRGBA_8888_SkColorType: + case kBGRA_8888_SkColorType: + return true; + default: + return false; + } +} + +static void pick_memory_stages(SkColorType ct, SkRasterPipeline::StockStage* load, + SkRasterPipeline::StockStage* store) { + switch(ct) { + case kUnknown_SkColorType: + case kAlpha_8_SkColorType: + case kARGB_4444_SkColorType: + case kIndex_8_SkColorType: + case kGray_8_SkColorType: + SkASSERT(false); + break; + case kRGB_565_SkColorType: + if (load) *load = SkRasterPipeline::load_565; + if (store) *store = SkRasterPipeline::store_565; + break; + case kRGBA_8888_SkColorType: + case kBGRA_8888_SkColorType: + if (load) *load = SkRasterPipeline::load_8888; + if (store) *store = SkRasterPipeline::store_8888; + break; + case kRGBA_F16_SkColorType: + if (load) *load = SkRasterPipeline::load_f16; + if (store) *store = SkRasterPipeline::store_f16; + break; + } +} + +static void blend_line(SkColorType dstCT, void* dst, + SkColorType srcCT, void* src, + bool needsSrgbToLinear, SkAlphaType at, + int width) { + // Setup conversion from the source and dest, which will be the same. + SkRasterPipeline convert_to_linear_premul; + if (needsSrgbToLinear) { + convert_to_linear_premul.append_from_srgb(at); + } + if (kUnpremul_SkAlphaType == at) { + // srcover assumes premultiplied inputs. + convert_to_linear_premul.append(SkRasterPipeline::premul); + } + + SkRasterPipeline p; + SkRasterPipeline::StockStage load_dst, store_dst; + pick_memory_stages(dstCT, &load_dst, &store_dst); + + // Load the final dst. + p.append(load_dst, dst); + p.extend(convert_to_linear_premul); + p.append(SkRasterPipeline::move_src_dst); + + // Load the src. + SkRasterPipeline::StockStage load_src; + pick_memory_stages(srcCT, &load_src, nullptr); + p.append(load_src, src); + if (dstCT != srcCT) { + SkASSERT(kBGRA_8888_SkColorType == srcCT); + p.append(SkRasterPipeline::swap_rb); + } + p.extend(convert_to_linear_premul); + + p.append(SkRasterPipeline::srcover); + + // Convert back to dst. + if (kUnpremul_SkAlphaType == at) { + p.append(SkRasterPipeline::unpremul); + } + if (needsSrgbToLinear) { + p.append(SkRasterPipeline::to_srgb); + } + p.append(store_dst, dst); + + p.run(0, width); +} + SkCodec::Result SkWebpCodec::onGetPixels(const SkImageInfo& dstInfo, void* dst, size_t rowBytes, const Options& options, SkPMColor*, int*, int* rowsDecodedPtr) { - if (!conversion_possible(dstInfo, this->getInfo()) || - !this->initializeColorXform(dstInfo, options.fPremulBehavior)) + // Ensure that we have parsed this far. + const int index = options.fFrameIndex; + if (index >= this->onGetFrameCount()) { + return kIncompleteInput; + } + + const auto& srcInfo = this->getInfo(); { - return kInvalidConversion; + auto info = srcInfo; + if (index > 0) { + auto alphaType = alpha_type(fFrameHolder.frame(index)->hasAlpha()); + info = info.makeAlphaType(alphaType); + } + if (!conversion_possible(dstInfo, info) || + !this->initializeColorXform(dstInfo, options.fPremulBehavior)) + { + return kInvalidConversion; + } + } + + if (index > 0 && (options.fSubset || dstInfo.dimensions() != srcInfo.dimensions())) { + // Subsetting and scaling are tricky when asking for frames beyond frame 0. In order to + // support it, we'll need to determine the proper rectangle for a + // WEBP_MUX_DISPOSE_BACKGROUND required frame before erasing it. (Currently the order + // is backwards.) Disable until it becomes clear that supporting it is important. + return kUnimplemented; } WebPDecoderConfig config; @@ -212,16 +423,46 @@ SkCodec::Result SkWebpCodec::onGetPixels(const SkImageInfo& dstInfo, void* dst, WebPIterator frame; SkAutoTCallVProc<WebPIterator, WebPDemuxReleaseIterator> autoFrame(&frame); - // If this succeeded in NewFromStream(), it should succeed again here. - SkAssertResult(WebPDemuxGetFrame(fDemux, 1, &frame)); + // If this succeeded in onGetFrameCount(), it should succeed again here. + SkAssertResult(WebPDemuxGetFrame(fDemux, index + 1, &frame)); + const int requiredFrame = index == 0 ? kNone : fFrameHolder.frame(index)->getRequiredFrame(); // Get the frameRect. libwebp will have already signaled an error if this is not fully // contained by the canvas. auto frameRect = SkIRect::MakeXYWH(frame.x_offset, frame.y_offset, frame.width, frame.height); - SkASSERT(this->getInfo().bounds().contains(frameRect)); - bool frameIsSubset = frameRect.size() != this->getInfo().dimensions(); - if (frameIsSubset) { - SkSampler::Fill(dstInfo, dst, rowBytes, 0, options.fZeroInitialized); + SkASSERT(srcInfo.bounds().contains(frameRect)); + const bool frameIsSubset = frameRect != srcInfo.bounds(); + if (kNone == requiredFrame) { + if (frameIsSubset) { + SkSampler::Fill(dstInfo, dst, rowBytes, 0, options.fZeroInitialized); + } + } else { + if (!options.fHasPriorFrame) { + Options prevFrameOpts(options); + prevFrameOpts.fFrameIndex = requiredFrame; + const auto result = this->getPixels(dstInfo, dst, rowBytes, &prevFrameOpts, + nullptr, nullptr); + switch (result) { + case kSuccess: + break; + case kIncompleteInput: + return kInvalidInput; + default: + return result; + } + } + + // Dispose bg color + const Frame* priorFrame = fFrameHolder.frame(requiredFrame); + if (priorFrame->getDisposalMethod() == SkCodecAnimation::RestoreBGColor_DisposalMethod) { + // FIXME: If we add support for scaling/subsets, this rectangle needs to be adjusted. + const auto priorRect = priorFrame->frameRect(); + const auto info = dstInfo.makeWH(priorRect.width(), priorRect.height()); + const size_t bpp = SkColorTypeBytesPerPixel(dstInfo.colorType()); + const size_t offset = priorRect.x() * bpp + priorRect.y() * rowBytes; + auto* eraseDst = SkTAddOffset<void>(dst, offset); + SkSampler::Fill(info, eraseDst, rowBytes, 0, kNo_ZeroInitialized); + } } int dstX = frameRect.x(); @@ -265,7 +506,7 @@ SkCodec::Result SkWebpCodec::onGetPixels(const SkImageInfo& dstInfo, void* dst, // Ignore the frame size and offset when determining if scaling is necessary. int scaledWidth = subsetWidth; int scaledHeight = subsetHeight; - SkISize srcSize = options.fSubset ? options.fSubset->size() : this->getInfo().dimensions(); + SkISize srcSize = options.fSubset ? options.fSubset->size() : srcInfo.dimensions(); if (srcSize != dstInfo.dimensions()) { config.options.use_scaling = 1; @@ -291,32 +532,55 @@ SkCodec::Result SkWebpCodec::onGetPixels(const SkImageInfo& dstInfo, void* dst, config.options.scaled_height = scaledHeight; } - // Swizzling between RGBA and BGRA is zero cost in a color transform. So when we have a - // color transform, we should decode to whatever is easiest for libwebp, and then let the - // color transform swizzle if necessary. - // Lossy webp is encoded as YUV (so RGBA and BGRA are the same cost). Lossless webp is - // encoded as BGRA. This means decoding to BGRA is either faster or the same cost as RGBA. - config.output.colorspace = this->colorXform() ? MODE_BGRA : - webp_decode_mode(dstInfo.colorType(), dstInfo.alphaType() == kPremul_SkAlphaType); + const bool blendWithPrevFrame = requiredFrame != kNone && frame.blend_method == WEBP_MUX_BLEND + && frame.has_alpha; + if (blendWithPrevFrame && options.fPremulBehavior == SkTransferFunctionBehavior::kRespect) { + // Blending is done with SkRasterPipeline, which requires a color space that is valid for + // rendering. + const auto* cs = dstInfo.colorSpace(); + if (!cs || (!cs->gammaCloseToSRGB() && !cs->gammaIsLinear())) { + return kInvalidConversion; + } + } + + SkBitmap webpDst; + if ((this->colorXform() && !is_8888(dstInfo.colorType())) || blendWithPrevFrame) { + // Swizzling between RGBA and BGRA is zero cost in a color transform. So when we have a + // color transform, we should decode to whatever is easiest for libwebp, and then let the + // color transform swizzle if necessary. + // Lossy webp is encoded as YUV (so RGBA and BGRA are the same cost). Lossless webp is + // encoded as BGRA. This means decoding to BGRA is either faster or the same cost as RGBA. + auto info = dstInfo.makeColorType(kBGRA_8888_SkColorType); + if (info.alphaType() == kPremul_SkAlphaType && this->colorXform()) { + info = info.makeAlphaType(kUnpremul_SkAlphaType); + } + + // We will decode the entire image and then perform the color transform. libwebp + // does not provide a row-by-row API. This is a shame particularly when we do not want + // 8888, since we will need to create another image sized buffer. + webpDst.allocPixels(info); + } else { + // libwebp can decode directly into the output memory. + auto info = dstInfo; + if (this->colorXform()) { + SkASSERT(is_8888(dstInfo.colorType())); + // As above, BGRA is faster or the same cost as RGBA for libwebp, + // and we're going to transform, so tell libwebp to use BGRA. + info = info.makeColorType(kBGRA_8888_SkColorType); + } + webpDst.installPixels(info, dst, rowBytes); + } + + if (!frame.has_alpha) { + webpDst.setAlphaType(kOpaque_SkAlphaType); + } + + config.output.colorspace = webp_decode_mode(webpDst.info()); config.output.is_external_memory = 1; - // We will decode the entire image and then perform the color transform. libwebp - // does not provide a row-by-row API. This is a shame particularly when we do not want - // 8888, since we will need to create another image sized buffer. - SkAutoTMalloc<uint32_t> pixels; - bool needsCopy = this->colorXform() && kRGBA_8888_SkColorType != dstInfo.colorType() && - kBGRA_8888_SkColorType != dstInfo.colorType(); - void* webpDst = needsCopy ? pixels.reset(dstInfo.width() * dstInfo.height()) : dst; - size_t webpRowBytes = needsCopy ? dstInfo.width() * sizeof(uint32_t) : rowBytes; - size_t totalBytes = needsCopy ? webpRowBytes * dstInfo.height() - : dstInfo.getSafeSize(webpRowBytes); - size_t dstBpp = SkColorTypeBytesPerPixel(dstInfo.colorType()); - size_t webpBpp = needsCopy ? sizeof(uint32_t) : dstBpp; - - size_t offset = dstX * webpBpp + dstY * webpRowBytes; - config.output.u.RGBA.rgba = SkTAddOffset<uint8_t>(webpDst, offset); - config.output.u.RGBA.stride = (int) webpRowBytes; - config.output.u.RGBA.size = totalBytes - offset; + config.output.u.RGBA.rgba = reinterpret_cast<uint8_t*>(webpDst.getAddr(dstX, dstY)); + config.output.u.RGBA.stride = static_cast<int>(webpDst.rowBytes()); + config.output.u.RGBA.size = webpDst.getSafeSize(); SkAutoTCallVProc<WebPIDecoder, WebPIDelete> idec(WebPIDecode(nullptr, 0, &config)); if (!idec) { @@ -339,21 +603,55 @@ SkCodec::Result SkWebpCodec::onGetPixels(const SkImageInfo& dstInfo, void* dst, return kInvalidInput; } + // We're only transforming the new part of the frame, so no need to worry about the + // final composited alpha. + const auto srcAlpha = 0 == index ? srcInfo.alphaType() : alpha_type(frame.has_alpha); + const auto xformAlphaType = select_xform_alpha(dstInfo.alphaType(), srcAlpha); + const bool needsSrgbToLinear = dstInfo.gammaCloseToSRGB() && + options.fPremulBehavior == SkTransferFunctionBehavior::kRespect; + + const size_t dstBpp = SkColorTypeBytesPerPixel(dstInfo.colorType()); + dst = SkTAddOffset<void>(dst, dstBpp * dstX + rowBytes * dstY); + const size_t srcRowBytes = config.output.u.RGBA.stride; + + const auto dstCT = dstInfo.colorType(); if (this->colorXform()) { - SkColorSpaceXform::ColorFormat dstColorFormat = select_xform_format(dstInfo.colorType()); - SkAlphaType xformAlphaType = select_xform_alpha(dstInfo.alphaType(), - this->getInfo().alphaType()); + const auto dstColorFormat = select_xform_format(dstInfo.colorType()); + const auto srcColorFormat = SkColorSpaceXform::kBGRA_8888_ColorFormat; + SkASSERT(select_xform_format(webpDst.colorType()) == srcColorFormat); uint32_t* xformSrc = (uint32_t*) config.output.u.RGBA.rgba; - void* xformDst = SkTAddOffset<void>(dst, dstBpp * dstX + rowBytes * dstY); - size_t srcRowBytes = config.output.u.RGBA.stride; + SkBitmap tmp; + void* xformDst; + + if (blendWithPrevFrame) { + // Xform into temporary bitmap big enough for one row. + tmp.allocPixels(dstInfo.makeWH(scaledWidth, 1)); + xformDst = tmp.getPixels(); + } else { + xformDst = dst; + } for (int y = 0; y < rowsDecoded; y++) { SkAssertResult(this->colorXform()->apply(dstColorFormat, xformDst, - SkColorSpaceXform::kBGRA_8888_ColorFormat, xformSrc, scaledWidth, - xformAlphaType)); - xformDst = SkTAddOffset<void>(xformDst, rowBytes); + srcColorFormat, xformSrc, scaledWidth, xformAlphaType)); + if (blendWithPrevFrame) { + blend_line(dstCT, &dst, dstCT, &xformDst, needsSrgbToLinear, xformAlphaType, + scaledWidth); + dst = SkTAddOffset<void>(dst, rowBytes); + } else { + xformDst = SkTAddOffset<void>(xformDst, rowBytes); + } xformSrc = SkTAddOffset<uint32_t>(xformSrc, srcRowBytes); } + } else if (blendWithPrevFrame) { + const uint8_t* src = config.output.u.RGBA.rgba; + + for (int y = 0; y < rowsDecoded; y++) { + blend_line(dstCT, &dst, webpDst.colorType(), &src, needsSrgbToLinear, + xformAlphaType, scaledWidth); + src = SkTAddOffset<const uint8_t>(src, srcRowBytes); + dst = SkTAddOffset<void>(dst, rowBytes); + } } return result; @@ -365,4 +663,7 @@ SkWebpCodec::SkWebpCodec(int width, int height, const SkEncodedInfo& info, : INHERITED(width, height, info, stream, std::move(colorSpace)) , fDemux(demux) , fData(std::move(data)) -{} + , fFailed(false) +{ + fFrameHolder.setScreenSize(width, height); +} diff --git a/src/codec/SkWebpCodec.h b/src/codec/SkWebpCodec.h index 93b60f646e..f4c7910ee6 100644 --- a/src/codec/SkWebpCodec.h +++ b/src/codec/SkWebpCodec.h @@ -11,9 +11,12 @@ #include "SkCodec.h" #include "SkColorSpace.h" #include "SkEncodedImageFormat.h" +#include "SkFrameHolder.h" #include "SkImageInfo.h" #include "SkTypes.h" +#include <vector> + class SkStream; extern "C" { struct WebPDemuxer; @@ -37,6 +40,11 @@ protected: bool onDimensionsSupported(const SkISize&) override; bool onGetValidSubset(SkIRect* /* desiredSubset */) const override; + + int onGetFrameCount() override; + bool onGetFrameInfo(int, FrameInfo*) const override; + int onGetRepetitionCount() override; + private: SkWebpCodec(int width, int height, const SkEncodedInfo&, sk_sp<SkColorSpace>, SkStream*, WebPDemuxer*, sk_sp<SkData>); @@ -47,6 +55,57 @@ private: // This should not be freed until the decode is completed. sk_sp<SkData> fData; + class Frame : public SkFrame { + public: + Frame(int i, bool alpha) + : INHERITED(i) + , fReportsAlpha(alpha) + {} + Frame(Frame&& other) + : INHERITED(other.frameId()) + , fReportsAlpha(other.fReportsAlpha) + {} + + protected: + bool onReportsAlpha() const override { + return fReportsAlpha; + } + + private: + const bool fReportsAlpha; + + typedef SkFrame INHERITED; + }; + + class FrameHolder : public SkFrameHolder { + public: + ~FrameHolder() override {} + void setScreenSize(int w, int h) { + fScreenWidth = w; + fScreenHeight = h; + } + Frame* appendNewFrame(bool hasAlpha); + const Frame* frame(int i) const; + int size() const { + return static_cast<int>(fFrames.size()); + } + void reserve(int size) { + fFrames.reserve(size); + } + + protected: + const SkFrame* onGetFrame(int i) const override; + + private: + std::vector<Frame> fFrames; + }; + + FrameHolder fFrameHolder; + // Set to true if WebPDemuxGetFrame fails. This only means + // that we will cap the frame count to the frames that + // succeed. + bool fFailed; + typedef SkCodec INHERITED; }; #endif // SkWebpCodec_DEFINED diff --git a/tests/CodecAnimTest.cpp b/tests/CodecAnimTest.cpp index 24b9c13ae9..087f5d89cd 100644 --- a/tests/CodecAnimTest.cpp +++ b/tests/CodecAnimTest.cpp @@ -34,11 +34,37 @@ static void write_bm(const char* name, const SkBitmap& bm) { DEF_TEST(Codec_trunc, r) { sk_sp<SkData> data(GetResourceAsData("box.gif")); + if (!data) { + return; + } data = SkData::MakeSubset(data.get(), 0, 23); std::unique_ptr<SkCodec> codec(SkCodec::NewFromData(data)); codec->getFrameInfo(); } +// 565 does not support alpha, but there is no reason for it not to support an +// animated image with a frame that has alpha but then blends onto an opaque +// frame making the result opaque. Test that we can decode such a frame. +DEF_TEST(Codec_565, r) { + sk_sp<SkData> data(GetResourceAsData("blendBG.webp")); + if (!data) { + return; + } + std::unique_ptr<SkCodec> codec(SkCodec::NewFromData(std::move(data))); + auto info = codec->getInfo().makeColorType(kRGB_565_SkColorType); + SkBitmap bm; + bm.allocPixels(info); + + SkCodec::Options options; + options.fFrameIndex = 1; + options.fHasPriorFrame = false; + + const auto result = codec->getPixels(info, bm.getPixels(), bm.rowBytes(), + &options, nullptr, nullptr); + REPORTER_ASSERT(r, result == SkCodec::kSuccess); +} + + DEF_TEST(Codec_frames, r) { #define kOpaque kOpaque_SkAlphaType #define kUnpremul kUnpremul_SkAlphaType @@ -97,6 +123,12 @@ DEF_TEST(Codec_frames, r) { { "mandrill.wbmp", 1, {}, {}, {}, 0 }, { "randPixels.bmp", 1, {}, {}, {}, 0 }, { "yellow_rose.webp", 1, {}, {}, {}, 0 }, + { "webp-animated.webp", 3, { 0, 1 }, { kOpaque, kOpaque, kOpaque }, + { 1000, 500, 1000 }, SkCodec::kRepetitionCountInfinite }, + { "blendBG.webp", 7, { 0, SkCodec::kNone, SkCodec::kNone, SkCodec::kNone, + 3, 3 }, + { kOpaque, kOpaque, kUnpremul, kOpaque, kUnpremul, kUnpremul }, + { 525, 500, 525, 437, 609, 729, 444 }, 7 }, }; #undef kOpaque #undef kUnpremul @@ -190,17 +222,6 @@ DEF_TEST(Codec_frames, r) { rec.fName, i, rec.fDurations[i], frameInfo.fDuration); } - if (0 == i) { - REPORTER_ASSERT(r, frameInfo.fRequiredFrame == SkCodec::kNone); - REPORTER_ASSERT(r, frameInfo.fAlphaType == codec->getInfo().alphaType()); - continue; - } - - if (rec.fRequiredFrames[i-1] != frameInfo.fRequiredFrame) { - ERRORF(r, "%s's frame %i has wrong dependency! expected: %i\tactual: %i", - rec.fName, i, rec.fRequiredFrames[i-1], frameInfo.fRequiredFrame); - } - auto to_string = [](SkAlphaType type) { switch (type) { case kUnpremul_SkAlphaType: @@ -212,12 +233,19 @@ DEF_TEST(Codec_frames, r) { } }; - auto expectedAlpha = rec.fAlphaTypes[i-1]; + auto expectedAlpha = 0 == i ? codec->getInfo().alphaType() : rec.fAlphaTypes[i-1]; auto alpha = frameInfo.fAlphaType; if (expectedAlpha != alpha) { ERRORF(r, "%s's frame %i has wrong alpha type! expected: %s\tactual: %s", rec.fName, i, to_string(expectedAlpha), to_string(alpha)); } + + if (0 == i) { + REPORTER_ASSERT(r, frameInfo.fRequiredFrame == SkCodec::kNone); + } else if (rec.fRequiredFrames[i-1] != frameInfo.fRequiredFrame) { + ERRORF(r, "%s's frame %i has wrong dependency! expected: %i\tactual: %i", + rec.fName, i, rec.fRequiredFrames[i-1], frameInfo.fRequiredFrame); + } } if (TestMode::kIndividual == mode) { diff --git a/third_party/gif/SkGifImageReader.cpp b/third_party/gif/SkGifImageReader.cpp index 8c5cfa1ea8..0666fedbc8 100644 --- a/third_party/gif/SkGifImageReader.cpp +++ b/third_party/gif/SkGifImageReader.cpp @@ -131,19 +131,19 @@ bool SkGIFLZWContext::outputRow(const unsigned char* rowBegin) drowEnd = drowStart + rowDup; // Extend if bottom edge isn't covered because of the shift upward. - if (((m_frameContext->height() - 1) - drowEnd) <= rowShift) + if ((unsigned)((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()) + if (drowEnd >= m_frameContext->height()) drowEnd = m_frameContext->height() - 1; } // Protect against too much image data. - if ((unsigned)drowStart >= m_frameContext->height()) + if (drowStart >= m_frameContext->height()) return true; // CALLBACK: Let the client know we have decoded a row. @@ -159,7 +159,7 @@ bool SkGIFLZWContext::outputRow(const unsigned char* rowBegin) switch (ipass) { case 1: irow += 8; - if (irow >= m_frameContext->height()) { + if (irow >= (unsigned) m_frameContext->height()) { ipass++; irow = 4; } @@ -167,7 +167,7 @@ bool SkGIFLZWContext::outputRow(const unsigned char* rowBegin) case 2: irow += 8; - if (irow >= m_frameContext->height()) { + if (irow >= (unsigned) m_frameContext->height()) { ipass++; irow = 2; } @@ -175,7 +175,7 @@ bool SkGIFLZWContext::outputRow(const unsigned char* rowBegin) case 3: irow += 4; - if (irow >= m_frameContext->height()) { + if (irow >= (unsigned) m_frameContext->height()) { ipass++; irow = 1; } @@ -183,7 +183,7 @@ bool SkGIFLZWContext::outputRow(const unsigned char* rowBegin) case 4: irow += 2; - if (irow >= m_frameContext->height()) { + if (irow >= (unsigned) m_frameContext->height()) { ipass++; irow = 0; } @@ -192,7 +192,7 @@ bool SkGIFLZWContext::outputRow(const unsigned char* rowBegin) default: break; } - } while (irow > (m_frameContext->height() - 1)); + } while (irow > (unsigned) (m_frameContext->height() - 1)); } return true; } @@ -491,8 +491,8 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) // 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); + fScreenWidth = GETINT16(currentComponent); + fScreenHeight = GETINT16(currentComponent + 2); const int globalColorMapColors = 2 << (currentComponent[4] & 0x07); @@ -623,7 +623,7 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) currentFrame->setDisposalMethod(SkCodecAnimation::Keep_DisposalMethod); break; } - currentFrame->setDelayTime(GETINT16(currentComponent + 1) * 10); + currentFrame->setDuration(GETINT16(currentComponent + 1) * 10); GETN(1, SkGIFConsumeBlock); break; } @@ -706,7 +706,7 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) } case SkGIFImageHeader: { - unsigned height, width, xOffset, yOffset; + int height, width, xOffset, yOffset; const unsigned char* currentComponent = reinterpret_cast<const unsigned char*>(m_streamBuffer.get()); @@ -730,8 +730,8 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) // 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); + fScreenHeight = std::max(fScreenHeight, yOffset + height); + fScreenWidth = std::max(fScreenWidth, xOffset + width); } // NOTE: Chromium placed this block after setHeaderDefined, down @@ -743,8 +743,8 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) // Work around more broken GIF files that have zero image width or // height. if (!height || !width) { - height = m_screenHeight; - width = m_screenWidth; + height = fScreenHeight; + width = fScreenWidth; if (!height || !width) { // This prevents attempting to continue reading this invalid stream. GETN(0, SkGIFDone); @@ -756,13 +756,17 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) // The three low-order bits of currentComponent[8] specify the bits per pixel. const int numColors = 2 << (currentComponent[8] & 0x7); if (currentFrameIsFirstFrame()) { - if (hasTransparentPixel(0, isLocalColormapDefined, numColors)) { + const int transPix = m_frames.empty() ? SkGIFColorMap::kNotFound + : m_frames[0]->transparentPixel(); + if (this->hasTransparency(transPix, + isLocalColormapDefined, numColors)) + { m_firstFrameHasAlpha = true; m_firstFrameSupportsIndex8 = true; } else { const bool frameIsSubset = xOffset > 0 || yOffset > 0 - || xOffset + width < m_screenWidth - || yOffset + height < m_screenHeight; + || width < fScreenWidth + || height < fScreenHeight; m_firstFrameHasAlpha = frameIsSubset; m_firstFrameSupportsIndex8 = !frameIsSubset; } @@ -780,7 +784,7 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) } - currentFrame->setRect(xOffset, yOffset, width, height); + currentFrame->setXYWH(xOffset, yOffset, width, height); currentFrame->setInterlaced(SkToBool(currentComponent[8] & 0x40)); // Overlaying interlaced, transparent GIFs over @@ -852,20 +856,14 @@ bool SkGifImageReader::parse(SkGifImageReader::SkGIFParseQuery query) return true; } -bool SkGifImageReader::hasTransparentPixel(int i, bool isLocalColormapDefined, - int localColors) { - SkASSERT(i >= 0); - if (m_frames.size() <= static_cast<size_t>(i)) { - // This should only happen when parsing the first frame. - SkASSERT(0 == i); - - // We did not see a Graphics Control Extension, so no transparent - // pixel was specified. But if there is no color table, this frame is - // still transparent. - return !isLocalColormapDefined && m_globalColorMap.numColors() == 0; +bool SkGifImageReader::hasTransparency(int transparentPixel, bool isLocalColormapDefined, + int localColors) const { + const int globalColors = m_globalColorMap.numColors(); + if (!isLocalColormapDefined && globalColors == 0) { + // No color table for this frame, so it is completely transparent. + return true; } - const int transparentPixel = m_frames[i]->transparentPixel(); if (transparentPixel < 0) { SkASSERT(SkGIFColorMap::kNotFound == transparentPixel); return false; @@ -875,16 +873,9 @@ bool SkGifImageReader::hasTransparentPixel(int i, bool isLocalColormapDefined, return transparentPixel < localColors; } - const int 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. - return true; - } - // If there is a global color table, it will be parsed before reaching // here. If its numColors is set, it will be defined. + SkASSERT(globalColors > 0); SkASSERT(m_globalColorMap.isDefined()); return transparentPixel < globalColors; } @@ -893,7 +884,7 @@ void SkGifImageReader::addFrameIfNecessary() { if (m_frames.empty() || m_frames.back()->isComplete()) { const size_t i = m_frames.size(); - std::unique_ptr<SkGIFFrameContext> frame(new SkGIFFrameContext(static_cast<int>(i))); + std::unique_ptr<SkGIFFrameContext> frame(new SkGIFFrameContext(this, static_cast<int>(i))); m_frames.push_back(std::move(frame)); } } @@ -907,38 +898,43 @@ static SkIRect frame_rect_on_screen(SkIRect frameRect, return frameRect; } -static bool independent(const SkGIFFrameContext& frame) { +static bool independent(const SkFrame& frame) { return frame.getRequiredFrame() == SkCodec::kNone; } -static bool restore_bg(const SkGIFFrameContext& frame) { +static bool restore_bg(const SkFrame& frame) { return frame.getDisposalMethod() == SkCodecAnimation::RestoreBGColor_DisposalMethod; } -void SkGifImageReader::setAlphaAndRequiredFrame(SkGIFFrameContext* frame) { +bool SkGIFFrameContext::onReportsAlpha() const { + // Note: We could correct these after decoding - i.e. some frames may turn out to be + // independent and opaque if they do not use the transparent pixel, but that would require + // checking whether each pixel used the transparent index. + return m_owner->hasTransparency(this->transparentPixel(), + m_localColorMap.isDefined(), m_localColorMap.numColors()); +} + +void SkFrameHolder::setAlphaAndRequiredFrame(SkFrame* frame) { + const bool reportsAlpha = frame->reportsAlpha(); + const auto screenRect = SkIRect::MakeWH(fScreenWidth, fScreenHeight); + const auto frameRect = frame_rect_on_screen(frame->frameRect(), screenRect); + const int i = frame->frameId(); if (0 == i) { - frame->setHasAlpha(m_firstFrameHasAlpha); + frame->setHasAlpha(reportsAlpha || frameRect != screenRect); frame->setRequiredFrame(SkCodec::kNone); return; } - // Note: We could correct these after decoding - i.e. some frames may turn out to be - // independent and opaque if they do not use the transparent pixel, but that would require - // checking whether each pixel used the transparent index. - const SkGIFColorMap& localMap = frame->localColorMap(); - const bool transValid = hasTransparentPixel(i, localMap.isDefined(), localMap.numColors()); - - const auto screenRect = SkIRect::MakeWH(m_screenWidth, m_screenHeight); - const auto frameRect = frame_rect_on_screen(frame->frameRect(), screenRect); - if (!transValid && frameRect == screenRect) { - frame->setHasAlpha(false); + const bool blendWithPrevFrame = frame->getBlend() == SkCodecAnimation::Blend::kPriorFrame; + if ((!reportsAlpha || !blendWithPrevFrame) && frameRect == screenRect) { + frame->setHasAlpha(reportsAlpha); frame->setRequiredFrame(SkCodec::kNone); return; } - const SkGIFFrameContext* prevFrame = m_frames[i - 1].get(); + const SkFrame* prevFrame = this->getFrame(i-1); while (prevFrame->getDisposalMethod() == SkCodecAnimation::RestorePrevious_DisposalMethod) { const int prevId = prevFrame->frameId(); if (0 == prevId) { @@ -947,7 +943,7 @@ void SkGifImageReader::setAlphaAndRequiredFrame(SkGIFFrameContext* frame) { return; } - prevFrame = m_frames[prevId - 1].get(); + prevFrame = this->getFrame(prevId - 1); } const bool clearPrevFrame = restore_bg(*prevFrame); @@ -961,7 +957,7 @@ void SkGifImageReader::setAlphaAndRequiredFrame(SkGIFFrameContext* frame) { } } - if (transValid) { + if (reportsAlpha && blendWithPrevFrame) { // Note: We could be more aggressive here. If prevFrame clears // to background color and covers its required frame (and that // frame is independent), prevFrame could be marked independent. @@ -979,7 +975,7 @@ void SkGifImageReader::setAlphaAndRequiredFrame(SkGIFFrameContext* frame) { return; } - prevFrame = m_frames[prevRequiredFrame].get(); + prevFrame = this->getFrame(prevRequiredFrame); prevFrameRect = frame_rect_on_screen(prevFrame->frameRect(), screenRect); } @@ -998,7 +994,7 @@ void SkGifImageReader::setAlphaAndRequiredFrame(SkGIFFrameContext* frame) { SkASSERT(prevFrame->getDisposalMethod() == SkCodecAnimation::Keep_DisposalMethod); frame->setRequiredFrame(prevFrame->frameId()); - frame->setHasAlpha(prevFrame->hasAlpha()); + frame->setHasAlpha(prevFrame->hasAlpha() || (reportsAlpha && !blendWithPrevFrame)); } // FIXME: Move this method to close to doLZW(). diff --git a/third_party/gif/SkGifImageReader.h b/third_party/gif/SkGifImageReader.h index 4667f7963f..6c313e923b 100644 --- a/third_party/gif/SkGifImageReader.h +++ b/third_party/gif/SkGifImageReader.h @@ -47,6 +47,7 @@ class SkGifCodec; #include "SkCodecAnimation.h" #include "SkColorTable.h" #include "SkData.h" +#include "SkFrameHolder.h" #include "SkImageInfo.h" #include "SkStreamBuffer.h" #include "../private/SkTArray.h" @@ -85,7 +86,7 @@ enum SkGIFState { SkGIFConsumeComment }; -struct SkGIFFrameContext; +class SkGIFFrameContext; class SkGIFColorMap; // LZW decoder state machine. @@ -191,19 +192,15 @@ private: mutable sk_sp<SkColorTable> m_table; }; +class SkGifImageReader; + // LocalFrame output state machine. -struct SkGIFFrameContext : SkNoncopyable { +class SkGIFFrameContext : public SkFrame { public: - SkGIFFrameContext(int id) - : m_frameId(id) - , m_xOffset(0) - , m_yOffset(0) - , m_width(0) - , m_height(0) + SkGIFFrameContext(SkGifImageReader* reader, int id) + : INHERITED(id) + , m_owner(reader) , m_transparentPixel(SkGIFColorMap::kNotFound) - , m_hasAlpha(false) - , m_disposalMethod(SkCodecAnimation::Keep_DisposalMethod) - , m_requiredFrame(kUninitialized) , m_dataSize(0) , m_progressiveDisplay(false) , m_interlaced(false) @@ -226,31 +223,8 @@ public: bool decode(SkStreamBuffer*, 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; } int transparentPixel() const { return m_transparentPixel; } void setTransparentPixel(int pixel) { m_transparentPixel = pixel; } - bool hasAlpha() const { return m_hasAlpha; } - void setHasAlpha(bool alpha) { m_hasAlpha = alpha; } - SkCodecAnimation::DisposalMethod getDisposalMethod() const { return m_disposalMethod; } - void setDisposalMethod(SkCodecAnimation::DisposalMethod disposalMethod) { m_disposalMethod = disposalMethod; } - - int getRequiredFrame() const { - SkASSERT(this->reachedStartOfData()); - return m_requiredFrame; - } - void setRequiredFrame(int req) { m_requiredFrame = req; } unsigned delayTime() const { return m_delayTime; } void setDelayTime(unsigned delay) { m_delayTime = delay; } @@ -274,24 +248,14 @@ public: const SkGIFColorMap& localColorMap() const { return m_localColorMap; } SkGIFColorMap& localColorMap() { return m_localColorMap; } - bool reachedStartOfData() const { return m_requiredFrame != kUninitialized; } +protected: + bool onReportsAlpha() const override; private: - static constexpr int kUninitialized = -2; + // Unowned pointer to the object that owns this frame. + const SkGifImageReader* m_owner; - int m_frameId; - unsigned m_xOffset; - unsigned m_yOffset; // With respect to "screen" origin. - unsigned m_width; - unsigned m_height; int m_transparentPixel; // Index of transparent pixel. Value is kNotFound if there is no transparent pixel. - // Cached value, taking into account: - // - m_transparentPixel - // - frameRect - // - previous required frame - bool m_hasAlpha; - SkCodecAnimation::DisposalMethod m_disposalMethod; // Restore to background, leave in place, etc. - int m_requiredFrame; int m_dataSize; bool m_progressiveDisplay; // If true, do Haeberli interlace hack. @@ -309,9 +273,11 @@ private: bool m_isComplete; bool m_isHeaderDefined; bool m_isDataSizeDefined; + + typedef SkFrame INHERITED; }; -class SkGifImageReader final : public SkNoncopyable { +class SkGifImageReader final : public SkFrameHolder { public: // This takes ownership of stream. SkGifImageReader(SkStream* stream) @@ -319,8 +285,6 @@ public: , m_state(SkGIFType) , 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) @@ -335,9 +299,6 @@ public: 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 SkGIFParseQuery { @@ -408,6 +369,16 @@ public: bool firstFrameSupportsIndex8() const { return m_firstFrameSupportsIndex8; } + // Helper function that returns whether an SkGIFFrameContext has transparency. + // This method is sometimes called before creating one/parsing its color map, + // so it cannot rely on SkGIFFrameContext::transparentPixel or ::localColorMap(). + bool hasTransparency(int transPix, bool hasLocalColorMap, int localMapColors) const; + +protected: + const SkFrame* onGetFrame(int i) const override { + return static_cast<const SkFrame*>(this->frameContext(i)); + } + private: // Requires that one byte has been buffered into m_streamBuffer. unsigned char getOneByte() const { @@ -415,11 +386,6 @@ private: } void addFrameIfNecessary(); - // Must be called *after* the SkGIFFrameContext's color table (if any) has been parsed. - void setAlphaAndRequiredFrame(SkGIFFrameContext*); - // This method is sometimes called before creating a SkGIFFrameContext, so it cannot rely - // on SkGIFFrameContext::localColorMap(). - bool hasTransparentPixel(int frameIndex, bool hasLocalColorMap, int localMapColors); bool currentFrameIsFirstFrame() const { return m_frames.empty() || (m_frames.size() == 1u && !m_frames[0]->isComplete()); @@ -434,8 +400,6 @@ private: // 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; SkGIFColorMap m_globalColorMap; static constexpr int cLoopCountNotSeen = -2; |