1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "ImageLogging.h" // Must appear first
8 #include "gfxPlatform.h"
9 #include "mozilla/TelemetryHistogramEnums.h"
10 #include "nsWebPDecoder.h"
11
12 #include "RasterImage.h"
13 #include "SurfacePipeFactory.h"
14
15 using namespace mozilla::gfx;
16
17 namespace mozilla {
18 namespace image {
19
20 static LazyLogModule sWebPLog("WebPDecoder");
21
nsWebPDecoder(RasterImage * aImage)22 nsWebPDecoder::nsWebPDecoder(RasterImage* aImage)
23 : Decoder(aImage),
24 mDecoder(nullptr),
25 mBlend(BlendMethod::OVER),
26 mDisposal(DisposalMethod::KEEP),
27 mTimeout(FrameTimeout::Forever()),
28 mFormat(SurfaceFormat::OS_RGBX),
29 mLastRow(0),
30 mCurrentFrame(0),
31 mData(nullptr),
32 mLength(0),
33 mIteratorComplete(false),
34 mNeedDemuxer(true),
35 mGotColorProfile(false) {
36 MOZ_LOG(sWebPLog, LogLevel::Debug,
37 ("[this=%p] nsWebPDecoder::nsWebPDecoder", this));
38 }
39
~nsWebPDecoder()40 nsWebPDecoder::~nsWebPDecoder() {
41 MOZ_LOG(sWebPLog, LogLevel::Debug,
42 ("[this=%p] nsWebPDecoder::~nsWebPDecoder", this));
43 if (mDecoder) {
44 WebPIDelete(mDecoder);
45 WebPFreeDecBuffer(&mBuffer);
46 }
47 }
48
ReadData()49 LexerResult nsWebPDecoder::ReadData() {
50 MOZ_ASSERT(mData);
51 MOZ_ASSERT(mLength > 0);
52
53 WebPDemuxer* demuxer = nullptr;
54 bool complete = mIteratorComplete;
55
56 if (mNeedDemuxer) {
57 WebPDemuxState state;
58 WebPData fragment;
59 fragment.bytes = mData;
60 fragment.size = mLength;
61
62 demuxer = WebPDemuxPartial(&fragment, &state);
63 if (state == WEBP_DEMUX_PARSE_ERROR) {
64 MOZ_LOG(
65 sWebPLog, LogLevel::Error,
66 ("[this=%p] nsWebPDecoder::ReadData -- demux parse error\n", this));
67 WebPDemuxDelete(demuxer);
68 return LexerResult(TerminalState::FAILURE);
69 }
70
71 if (state == WEBP_DEMUX_PARSING_HEADER) {
72 WebPDemuxDelete(demuxer);
73 return LexerResult(Yield::NEED_MORE_DATA);
74 }
75
76 if (!demuxer) {
77 MOZ_LOG(sWebPLog, LogLevel::Error,
78 ("[this=%p] nsWebPDecoder::ReadData -- no demuxer\n", this));
79 return LexerResult(TerminalState::FAILURE);
80 }
81
82 complete = complete || state == WEBP_DEMUX_DONE;
83 }
84
85 LexerResult rv(TerminalState::FAILURE);
86 if (!HasSize()) {
87 rv = ReadHeader(demuxer, complete);
88 } else {
89 rv = ReadPayload(demuxer, complete);
90 }
91
92 WebPDemuxDelete(demuxer);
93 return rv;
94 }
95
DoDecode(SourceBufferIterator & aIterator,IResumable * aOnResume)96 LexerResult nsWebPDecoder::DoDecode(SourceBufferIterator& aIterator,
97 IResumable* aOnResume) {
98 while (true) {
99 SourceBufferIterator::State state = SourceBufferIterator::COMPLETE;
100 if (!mIteratorComplete) {
101 state = aIterator.AdvanceOrScheduleResume(SIZE_MAX, aOnResume);
102
103 // We need to remember since we can't advance a complete iterator.
104 mIteratorComplete = state == SourceBufferIterator::COMPLETE;
105 }
106
107 if (state == SourceBufferIterator::WAITING) {
108 return LexerResult(Yield::NEED_MORE_DATA);
109 }
110
111 LexerResult rv = UpdateBuffer(aIterator, state);
112 if (rv.is<Yield>() && rv.as<Yield>() == Yield::NEED_MORE_DATA) {
113 // We need to check the iterator to see if more is available before
114 // giving up unless we are already complete.
115 if (mIteratorComplete) {
116 MOZ_LOG(sWebPLog, LogLevel::Error,
117 ("[this=%p] nsWebPDecoder::DoDecode -- read all data, "
118 "but needs more\n",
119 this));
120 return LexerResult(TerminalState::FAILURE);
121 }
122 continue;
123 }
124
125 return rv;
126 }
127 }
128
UpdateBuffer(SourceBufferIterator & aIterator,SourceBufferIterator::State aState)129 LexerResult nsWebPDecoder::UpdateBuffer(SourceBufferIterator& aIterator,
130 SourceBufferIterator::State aState) {
131 MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
132
133 switch (aState) {
134 case SourceBufferIterator::READY:
135 if (!aIterator.IsContiguous()) {
136 // We need to buffer. This should be rare, but expensive.
137 break;
138 }
139 if (!mData) {
140 // For as long as we hold onto an iterator, we know the data pointers
141 // to the chunks cannot change underneath us, so save the pointer to
142 // the first block.
143 MOZ_ASSERT(mLength == 0);
144 mData = reinterpret_cast<const uint8_t*>(aIterator.Data());
145 }
146 mLength += aIterator.Length();
147 return ReadData();
148 case SourceBufferIterator::COMPLETE:
149 if (!mData) {
150 // We must have hit an error, such as an OOM, when buffering the
151 // first set of encoded data.
152 MOZ_LOG(
153 sWebPLog, LogLevel::Error,
154 ("[this=%p] nsWebPDecoder::DoDecode -- complete no data\n", this));
155 return LexerResult(TerminalState::FAILURE);
156 }
157 return ReadData();
158 default:
159 MOZ_LOG(sWebPLog, LogLevel::Error,
160 ("[this=%p] nsWebPDecoder::DoDecode -- bad state\n", this));
161 return LexerResult(TerminalState::FAILURE);
162 }
163
164 // We need to buffer. If we have no data buffered, we need to get everything
165 // from the first chunk of the source buffer before appending the new data.
166 if (mBufferedData.empty()) {
167 MOZ_ASSERT(mData);
168 MOZ_ASSERT(mLength > 0);
169
170 if (!mBufferedData.append(mData, mLength)) {
171 MOZ_LOG(sWebPLog, LogLevel::Error,
172 ("[this=%p] nsWebPDecoder::DoDecode -- oom, initialize %zu\n",
173 this, mLength));
174 return LexerResult(TerminalState::FAILURE);
175 }
176
177 MOZ_LOG(sWebPLog, LogLevel::Debug,
178 ("[this=%p] nsWebPDecoder::DoDecode -- buffered %zu bytes\n", this,
179 mLength));
180 }
181
182 // Append the incremental data from the iterator.
183 if (!mBufferedData.append(aIterator.Data(), aIterator.Length())) {
184 MOZ_LOG(sWebPLog, LogLevel::Error,
185 ("[this=%p] nsWebPDecoder::DoDecode -- oom, append %zu on %zu\n",
186 this, aIterator.Length(), mBufferedData.length()));
187 return LexerResult(TerminalState::FAILURE);
188 }
189
190 MOZ_LOG(sWebPLog, LogLevel::Debug,
191 ("[this=%p] nsWebPDecoder::DoDecode -- buffered %zu -> %zu bytes\n",
192 this, aIterator.Length(), mBufferedData.length()));
193 mData = mBufferedData.begin();
194 mLength = mBufferedData.length();
195 return ReadData();
196 }
197
CreateFrame(const OrientedIntRect & aFrameRect)198 nsresult nsWebPDecoder::CreateFrame(const OrientedIntRect& aFrameRect) {
199 MOZ_ASSERT(HasSize());
200 MOZ_ASSERT(!mDecoder);
201
202 MOZ_LOG(
203 sWebPLog, LogLevel::Debug,
204 ("[this=%p] nsWebPDecoder::CreateFrame -- frame %u, (%d, %d) %d x %d\n",
205 this, mCurrentFrame, aFrameRect.x, aFrameRect.y, aFrameRect.width,
206 aFrameRect.height));
207
208 if (aFrameRect.width <= 0 || aFrameRect.height <= 0) {
209 MOZ_LOG(sWebPLog, LogLevel::Error,
210 ("[this=%p] nsWebPDecoder::CreateFrame -- bad frame rect\n", this));
211 return NS_ERROR_FAILURE;
212 }
213
214 // If this is our first frame in an animation and it doesn't cover the
215 // full frame, then we are transparent even if there is no alpha
216 if (mCurrentFrame == 0 && !aFrameRect.IsEqualEdges(FullFrame())) {
217 MOZ_ASSERT(HasAnimation());
218 mFormat = SurfaceFormat::OS_RGBA;
219 PostHasTransparency();
220 }
221
222 WebPInitDecBuffer(&mBuffer);
223
224 switch (SurfaceFormat::OS_RGBA) {
225 case SurfaceFormat::B8G8R8A8:
226 mBuffer.colorspace = MODE_BGRA;
227 break;
228 case SurfaceFormat::A8R8G8B8:
229 mBuffer.colorspace = MODE_ARGB;
230 break;
231 case SurfaceFormat::R8G8B8A8:
232 mBuffer.colorspace = MODE_RGBA;
233 break;
234 default:
235 MOZ_ASSERT_UNREACHABLE("Unknown OS_RGBA");
236 return NS_ERROR_FAILURE;
237 }
238
239 mDecoder = WebPINewDecoder(&mBuffer);
240 if (!mDecoder) {
241 MOZ_LOG(sWebPLog, LogLevel::Error,
242 ("[this=%p] nsWebPDecoder::CreateFrame -- create decoder error\n",
243 this));
244 return NS_ERROR_FAILURE;
245 }
246
247 // WebP doesn't guarantee that the alpha generated matches the hint in the
248 // header, so we always need to claim the input is BGRA. If the output is
249 // BGRX, swizzling will mask off the alpha channel.
250 #if MOZ_BIG_ENDIAN()
251 mBuffer.colorspace = MODE_ARGB;
252 SurfaceFormat inFormat = mFormat;
253 #else
254 SurfaceFormat inFormat = SurfaceFormat::OS_RGBA;
255 #endif
256
257 SurfacePipeFlags pipeFlags = SurfacePipeFlags();
258 if (mFormat == SurfaceFormat::OS_RGBA &&
259 !(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA)) {
260 pipeFlags |= SurfacePipeFlags::PREMULTIPLY_ALPHA;
261 }
262
263 Maybe<AnimationParams> animParams;
264 if (!IsFirstFrameDecode()) {
265 animParams.emplace(aFrameRect.ToUnknownRect(), mTimeout, mCurrentFrame,
266 mBlend, mDisposal);
267 }
268
269 Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateSurfacePipe(
270 this, Size(), OutputSize(), aFrameRect, inFormat, mFormat, animParams,
271 mTransform, pipeFlags);
272 if (!pipe) {
273 MOZ_LOG(sWebPLog, LogLevel::Error,
274 ("[this=%p] nsWebPDecoder::CreateFrame -- no pipe\n", this));
275 return NS_ERROR_FAILURE;
276 }
277
278 mFrameRect = aFrameRect;
279 mPipe = std::move(*pipe);
280 return NS_OK;
281 }
282
EndFrame()283 void nsWebPDecoder::EndFrame() {
284 MOZ_ASSERT(HasSize());
285 MOZ_ASSERT(mDecoder);
286
287 auto opacity = mFormat == SurfaceFormat::OS_RGBA ? Opacity::SOME_TRANSPARENCY
288 : Opacity::FULLY_OPAQUE;
289
290 MOZ_LOG(sWebPLog, LogLevel::Debug,
291 ("[this=%p] nsWebPDecoder::EndFrame -- frame %u, opacity %d, "
292 "disposal %d, timeout %d, blend %d\n",
293 this, mCurrentFrame, (int)opacity, (int)mDisposal,
294 mTimeout.AsEncodedValueDeprecated(), (int)mBlend));
295
296 PostFrameStop(opacity);
297 WebPIDelete(mDecoder);
298 WebPFreeDecBuffer(&mBuffer);
299 mDecoder = nullptr;
300 mLastRow = 0;
301 ++mCurrentFrame;
302 }
303
ApplyColorProfile(const char * aProfile,size_t aLength)304 void nsWebPDecoder::ApplyColorProfile(const char* aProfile, size_t aLength) {
305 MOZ_ASSERT(!mGotColorProfile);
306 mGotColorProfile = true;
307
308 if (mCMSMode == CMSMode::Off || !GetCMSOutputProfile() ||
309 (mCMSMode == CMSMode::TaggedOnly && !aProfile)) {
310 return;
311 }
312
313 if (!aProfile) {
314 MOZ_LOG(sWebPLog, LogLevel::Debug,
315 ("[this=%p] nsWebPDecoder::ApplyColorProfile -- not tagged, use "
316 "sRGB transform\n",
317 this));
318 mTransform = GetCMSsRGBTransform(SurfaceFormat::OS_RGBA);
319 return;
320 }
321
322 mInProfile = qcms_profile_from_memory(aProfile, aLength);
323 if (!mInProfile) {
324 MOZ_LOG(
325 sWebPLog, LogLevel::Error,
326 ("[this=%p] nsWebPDecoder::ApplyColorProfile -- bad color profile\n",
327 this));
328 return;
329 }
330
331 uint32_t profileSpace = qcms_profile_get_color_space(mInProfile);
332 if (profileSpace != icSigRgbData) {
333 // WebP doesn't produce grayscale data, this must be corrupt.
334 MOZ_LOG(sWebPLog, LogLevel::Error,
335 ("[this=%p] nsWebPDecoder::ApplyColorProfile -- ignoring non-rgb "
336 "color profile\n",
337 this));
338 return;
339 }
340
341 // Calculate rendering intent.
342 int intent = gfxPlatform::GetRenderingIntent();
343 if (intent == -1) {
344 intent = qcms_profile_get_rendering_intent(mInProfile);
345 }
346
347 // Create the color management transform.
348 qcms_data_type type = gfxPlatform::GetCMSOSRGBAType();
349 mTransform = qcms_transform_create(mInProfile, type, GetCMSOutputProfile(),
350 type, (qcms_intent)intent);
351 MOZ_LOG(sWebPLog, LogLevel::Debug,
352 ("[this=%p] nsWebPDecoder::ApplyColorProfile -- use tagged "
353 "transform\n",
354 this));
355 }
356
ReadHeader(WebPDemuxer * aDemuxer,bool aIsComplete)357 LexerResult nsWebPDecoder::ReadHeader(WebPDemuxer* aDemuxer, bool aIsComplete) {
358 MOZ_ASSERT(aDemuxer);
359
360 MOZ_LOG(
361 sWebPLog, LogLevel::Debug,
362 ("[this=%p] nsWebPDecoder::ReadHeader -- %zu bytes\n", this, mLength));
363
364 uint32_t flags = WebPDemuxGetI(aDemuxer, WEBP_FF_FORMAT_FLAGS);
365
366 if (!IsMetadataDecode() && !mGotColorProfile) {
367 if (flags & WebPFeatureFlags::ICCP_FLAG) {
368 WebPChunkIterator iter;
369 if (!WebPDemuxGetChunk(aDemuxer, "ICCP", 1, &iter)) {
370 return aIsComplete ? LexerResult(TerminalState::FAILURE)
371 : LexerResult(Yield::NEED_MORE_DATA);
372 }
373
374 ApplyColorProfile(reinterpret_cast<const char*>(iter.chunk.bytes),
375 iter.chunk.size);
376 WebPDemuxReleaseChunkIterator(&iter);
377 } else {
378 ApplyColorProfile(nullptr, 0);
379 }
380 }
381
382 if (flags & WebPFeatureFlags::ANIMATION_FLAG) {
383 // A metadata decode expects to get the correct first frame timeout which
384 // sadly is not provided by the normal WebP header parsing.
385 WebPIterator iter;
386 if (!WebPDemuxGetFrame(aDemuxer, 1, &iter)) {
387 return aIsComplete ? LexerResult(TerminalState::FAILURE)
388 : LexerResult(Yield::NEED_MORE_DATA);
389 }
390
391 PostIsAnimated(FrameTimeout::FromRawMilliseconds(iter.duration));
392 WebPDemuxReleaseIterator(&iter);
393 } else {
394 // Single frames don't need a demuxer to be created.
395 mNeedDemuxer = false;
396 }
397
398 uint32_t width = WebPDemuxGetI(aDemuxer, WEBP_FF_CANVAS_WIDTH);
399 uint32_t height = WebPDemuxGetI(aDemuxer, WEBP_FF_CANVAS_HEIGHT);
400 if (width > INT32_MAX || height > INT32_MAX) {
401 return LexerResult(TerminalState::FAILURE);
402 }
403
404 PostSize(width, height);
405
406 bool alpha = flags & WebPFeatureFlags::ALPHA_FLAG;
407 if (alpha) {
408 mFormat = SurfaceFormat::OS_RGBA;
409 PostHasTransparency();
410 }
411
412 MOZ_LOG(sWebPLog, LogLevel::Debug,
413 ("[this=%p] nsWebPDecoder::ReadHeader -- %u x %u, alpha %d, "
414 "animation %d, metadata decode %d, first frame decode %d\n",
415 this, width, height, alpha, HasAnimation(), IsMetadataDecode(),
416 IsFirstFrameDecode()));
417
418 if (IsMetadataDecode()) {
419 return LexerResult(TerminalState::SUCCESS);
420 }
421
422 return ReadPayload(aDemuxer, aIsComplete);
423 }
424
ReadPayload(WebPDemuxer * aDemuxer,bool aIsComplete)425 LexerResult nsWebPDecoder::ReadPayload(WebPDemuxer* aDemuxer,
426 bool aIsComplete) {
427 if (!HasAnimation()) {
428 auto rv = ReadSingle(mData, mLength, FullFrame());
429 if (rv.is<TerminalState>() &&
430 rv.as<TerminalState>() == TerminalState::SUCCESS) {
431 PostDecodeDone();
432 }
433 return rv;
434 }
435 return ReadMultiple(aDemuxer, aIsComplete);
436 }
437
ReadSingle(const uint8_t * aData,size_t aLength,const OrientedIntRect & aFrameRect)438 LexerResult nsWebPDecoder::ReadSingle(const uint8_t* aData, size_t aLength,
439 const OrientedIntRect& aFrameRect) {
440 MOZ_ASSERT(!IsMetadataDecode());
441 MOZ_ASSERT(aData);
442 MOZ_ASSERT(aLength > 0);
443
444 MOZ_LOG(
445 sWebPLog, LogLevel::Debug,
446 ("[this=%p] nsWebPDecoder::ReadSingle -- %zu bytes\n", this, aLength));
447
448 if (!mDecoder && NS_FAILED(CreateFrame(aFrameRect))) {
449 return LexerResult(TerminalState::FAILURE);
450 }
451
452 bool complete;
453 do {
454 VP8StatusCode status = WebPIUpdate(mDecoder, aData, aLength);
455 switch (status) {
456 case VP8_STATUS_OK:
457 complete = true;
458 break;
459 case VP8_STATUS_SUSPENDED:
460 complete = false;
461 break;
462 default:
463 MOZ_LOG(sWebPLog, LogLevel::Error,
464 ("[this=%p] nsWebPDecoder::ReadSingle -- append error %d\n",
465 this, status));
466 return LexerResult(TerminalState::FAILURE);
467 }
468
469 int lastRow = -1;
470 int width = 0;
471 int height = 0;
472 int stride = 0;
473 uint8_t* rowStart =
474 WebPIDecGetRGB(mDecoder, &lastRow, &width, &height, &stride);
475
476 MOZ_LOG(
477 sWebPLog, LogLevel::Debug,
478 ("[this=%p] nsWebPDecoder::ReadSingle -- complete %d, read %d rows, "
479 "has %d rows available\n",
480 this, complete, mLastRow, lastRow));
481
482 if (!rowStart || lastRow == -1 || lastRow == mLastRow) {
483 return LexerResult(Yield::NEED_MORE_DATA);
484 }
485
486 if (width != mFrameRect.width || height != mFrameRect.height ||
487 stride < mFrameRect.width * 4 || lastRow > mFrameRect.height) {
488 MOZ_LOG(sWebPLog, LogLevel::Error,
489 ("[this=%p] nsWebPDecoder::ReadSingle -- bad (w,h,s) = (%d, %d, "
490 "%d)\n",
491 this, width, height, stride));
492 return LexerResult(TerminalState::FAILURE);
493 }
494
495 for (int row = mLastRow; row < lastRow; row++) {
496 uint32_t* src = reinterpret_cast<uint32_t*>(rowStart + row * stride);
497 WriteState result = mPipe.WriteBuffer(src);
498
499 Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect();
500 if (invalidRect) {
501 PostInvalidation(invalidRect->mInputSpaceRect,
502 Some(invalidRect->mOutputSpaceRect));
503 }
504
505 if (result == WriteState::FAILURE) {
506 MOZ_LOG(sWebPLog, LogLevel::Error,
507 ("[this=%p] nsWebPDecoder::ReadSingle -- write pixels error\n",
508 this));
509 return LexerResult(TerminalState::FAILURE);
510 }
511
512 if (result == WriteState::FINISHED) {
513 MOZ_ASSERT(row == lastRow - 1, "There was more data to read?");
514 complete = true;
515 break;
516 }
517 }
518
519 mLastRow = lastRow;
520 } while (!complete);
521
522 if (!complete) {
523 return LexerResult(Yield::NEED_MORE_DATA);
524 }
525
526 EndFrame();
527 return LexerResult(TerminalState::SUCCESS);
528 }
529
ReadMultiple(WebPDemuxer * aDemuxer,bool aIsComplete)530 LexerResult nsWebPDecoder::ReadMultiple(WebPDemuxer* aDemuxer,
531 bool aIsComplete) {
532 MOZ_ASSERT(!IsMetadataDecode());
533 MOZ_ASSERT(aDemuxer);
534
535 MOZ_LOG(sWebPLog, LogLevel::Debug,
536 ("[this=%p] nsWebPDecoder::ReadMultiple\n", this));
537
538 bool complete = aIsComplete;
539 WebPIterator iter;
540 auto rv = LexerResult(Yield::NEED_MORE_DATA);
541 if (WebPDemuxGetFrame(aDemuxer, mCurrentFrame + 1, &iter)) {
542 switch (iter.blend_method) {
543 case WEBP_MUX_BLEND:
544 mBlend = BlendMethod::OVER;
545 break;
546 case WEBP_MUX_NO_BLEND:
547 mBlend = BlendMethod::SOURCE;
548 break;
549 default:
550 MOZ_ASSERT_UNREACHABLE("Unhandled blend method");
551 break;
552 }
553
554 switch (iter.dispose_method) {
555 case WEBP_MUX_DISPOSE_NONE:
556 mDisposal = DisposalMethod::KEEP;
557 break;
558 case WEBP_MUX_DISPOSE_BACKGROUND:
559 mDisposal = DisposalMethod::CLEAR;
560 break;
561 default:
562 MOZ_ASSERT_UNREACHABLE("Unhandled dispose method");
563 break;
564 }
565
566 mFormat = iter.has_alpha || mCurrentFrame > 0 ? SurfaceFormat::OS_RGBA
567 : SurfaceFormat::OS_RGBX;
568 mTimeout = FrameTimeout::FromRawMilliseconds(iter.duration);
569 OrientedIntRect frameRect(iter.x_offset, iter.y_offset, iter.width,
570 iter.height);
571
572 rv = ReadSingle(iter.fragment.bytes, iter.fragment.size, frameRect);
573 complete = complete && !WebPDemuxNextFrame(&iter);
574 WebPDemuxReleaseIterator(&iter);
575 }
576
577 if (rv.is<TerminalState>() &&
578 rv.as<TerminalState>() == TerminalState::SUCCESS) {
579 // If we extracted one frame, and it is not the last, we need to yield to
580 // the lexer to allow the upper layers to acknowledge the frame.
581 if (!complete && !IsFirstFrameDecode()) {
582 rv = LexerResult(Yield::OUTPUT_AVAILABLE);
583 } else {
584 uint32_t loopCount = WebPDemuxGetI(aDemuxer, WEBP_FF_LOOP_COUNT);
585
586 MOZ_LOG(sWebPLog, LogLevel::Debug,
587 ("[this=%p] nsWebPDecoder::ReadMultiple -- loop count %u\n", this,
588 loopCount));
589 PostDecodeDone(loopCount - 1);
590 }
591 }
592
593 return rv;
594 }
595
SpeedHistogram() const596 Maybe<Telemetry::HistogramID> nsWebPDecoder::SpeedHistogram() const {
597 return Some(Telemetry::IMAGE_DECODE_SPEED_WEBP);
598 }
599
600 } // namespace image
601 } // namespace mozilla
602