• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2011 Google Inc. All Rights Reserved.
2 //
3 // Use of this source code is governed by a BSD-style license
4 // that can be found in the COPYING file in the root of the source
5 // tree. An additional intellectual property rights grant can be found
6 // in the file PATENTS. All contributing project authors may
7 // be found in the AUTHORS file in the root of the source tree.
8 // -----------------------------------------------------------------------------
9 //
10 // Read APIs for mux.
11 //
12 // Authors: Urvang (urvang@google.com)
13 //          Vikas (vikasa@google.com)
14 
15 #include <assert.h>
16 #include "./muxi.h"
17 #include "../utils/utils.h"
18 
19 //------------------------------------------------------------------------------
20 // Helper method(s).
21 
22 // Handy MACRO.
23 #define SWITCH_ID_LIST(INDEX, LIST)                                           \
24   if (idx == (INDEX)) {                                                       \
25     const WebPChunk* const chunk = ChunkSearchList((LIST), nth,               \
26                                                    kChunks[(INDEX)].tag);     \
27     if (chunk) {                                                              \
28       *data = chunk->data_;                                                   \
29       return WEBP_MUX_OK;                                                     \
30     } else {                                                                  \
31       return WEBP_MUX_NOT_FOUND;                                              \
32     }                                                                         \
33   }
34 
MuxGet(const WebPMux * const mux,CHUNK_INDEX idx,uint32_t nth,WebPData * const data)35 static WebPMuxError MuxGet(const WebPMux* const mux, CHUNK_INDEX idx,
36                            uint32_t nth, WebPData* const data) {
37   assert(mux != NULL);
38   assert(!IsWPI(kChunks[idx].id));
39   WebPDataInit(data);
40 
41   SWITCH_ID_LIST(IDX_VP8X, mux->vp8x_);
42   SWITCH_ID_LIST(IDX_ICCP, mux->iccp_);
43   SWITCH_ID_LIST(IDX_ANIM, mux->anim_);
44   SWITCH_ID_LIST(IDX_EXIF, mux->exif_);
45   SWITCH_ID_LIST(IDX_XMP, mux->xmp_);
46   SWITCH_ID_LIST(IDX_UNKNOWN, mux->unknown_);
47   return WEBP_MUX_NOT_FOUND;
48 }
49 #undef SWITCH_ID_LIST
50 
51 // Fill the chunk with the given data (includes chunk header bytes), after some
52 // verifications.
ChunkVerifyAndAssign(WebPChunk * chunk,const uint8_t * data,size_t data_size,size_t riff_size,int copy_data)53 static WebPMuxError ChunkVerifyAndAssign(WebPChunk* chunk,
54                                          const uint8_t* data, size_t data_size,
55                                          size_t riff_size, int copy_data) {
56   uint32_t chunk_size;
57   WebPData chunk_data;
58 
59   // Sanity checks.
60   if (data_size < CHUNK_HEADER_SIZE) return WEBP_MUX_NOT_ENOUGH_DATA;
61   chunk_size = GetLE32(data + TAG_SIZE);
62 
63   {
64     const size_t chunk_disk_size = SizeWithPadding(chunk_size);
65     if (chunk_disk_size > riff_size) return WEBP_MUX_BAD_DATA;
66     if (chunk_disk_size > data_size) return WEBP_MUX_NOT_ENOUGH_DATA;
67   }
68 
69   // Data assignment.
70   chunk_data.bytes = data + CHUNK_HEADER_SIZE;
71   chunk_data.size = chunk_size;
72   return ChunkAssignData(chunk, &chunk_data, copy_data, GetLE32(data + 0));
73 }
74 
MuxImageFinalize(WebPMuxImage * const wpi)75 int MuxImageFinalize(WebPMuxImage* const wpi) {
76   const WebPChunk* const img = wpi->img_;
77   const WebPData* const image = &img->data_;
78   const int is_lossless = (img->tag_ == kChunks[IDX_VP8L].tag);
79   int w, h;
80   int vp8l_has_alpha = 0;
81   const int ok = is_lossless ?
82       VP8LGetInfo(image->bytes, image->size, &w, &h, &vp8l_has_alpha) :
83       VP8GetInfo(image->bytes, image->size, image->size, &w, &h);
84   assert(img != NULL);
85   if (ok) {
86     // Ignore ALPH chunk accompanying VP8L.
87     if (is_lossless && (wpi->alpha_ != NULL)) {
88       ChunkDelete(wpi->alpha_);
89       wpi->alpha_ = NULL;
90     }
91     wpi->width_ = w;
92     wpi->height_ = h;
93     wpi->has_alpha_ = vp8l_has_alpha || (wpi->alpha_ != NULL);
94   }
95   return ok;
96 }
97 
MuxImageParse(const WebPChunk * const chunk,int copy_data,WebPMuxImage * const wpi)98 static int MuxImageParse(const WebPChunk* const chunk, int copy_data,
99                          WebPMuxImage* const wpi) {
100   const uint8_t* bytes = chunk->data_.bytes;
101   size_t size = chunk->data_.size;
102   const uint8_t* const last = bytes + size;
103   WebPChunk subchunk;
104   size_t subchunk_size;
105   ChunkInit(&subchunk);
106 
107   assert(chunk->tag_ == kChunks[IDX_ANMF].tag);
108   assert(!wpi->is_partial_);
109 
110   // ANMF.
111   {
112     const size_t hdr_size = ANMF_CHUNK_SIZE;
113     const WebPData temp = { bytes, hdr_size };
114     // Each of ANMF chunk contain a header at the beginning. So, its size should
115     // be at least 'hdr_size'.
116     if (size < hdr_size) goto Fail;
117     ChunkAssignData(&subchunk, &temp, copy_data, chunk->tag_);
118   }
119   ChunkSetNth(&subchunk, &wpi->header_, 1);
120   wpi->is_partial_ = 1;  // Waiting for ALPH and/or VP8/VP8L chunks.
121 
122   // Rest of the chunks.
123   subchunk_size = ChunkDiskSize(&subchunk) - CHUNK_HEADER_SIZE;
124   bytes += subchunk_size;
125   size -= subchunk_size;
126 
127   while (bytes != last) {
128     ChunkInit(&subchunk);
129     if (ChunkVerifyAndAssign(&subchunk, bytes, size, size,
130                              copy_data) != WEBP_MUX_OK) {
131       goto Fail;
132     }
133     switch (ChunkGetIdFromTag(subchunk.tag_)) {
134       case WEBP_CHUNK_ALPHA:
135         if (wpi->alpha_ != NULL) goto Fail;  // Consecutive ALPH chunks.
136         if (ChunkSetNth(&subchunk, &wpi->alpha_, 1) != WEBP_MUX_OK) goto Fail;
137         wpi->is_partial_ = 1;  // Waiting for a VP8 chunk.
138         break;
139       case WEBP_CHUNK_IMAGE:
140         if (ChunkSetNth(&subchunk, &wpi->img_, 1) != WEBP_MUX_OK) goto Fail;
141         if (!MuxImageFinalize(wpi)) goto Fail;
142         wpi->is_partial_ = 0;  // wpi is completely filled.
143         break;
144       case WEBP_CHUNK_UNKNOWN:
145         if (wpi->is_partial_) goto Fail;  // Encountered an unknown chunk
146                                           // before some image chunks.
147         if (ChunkSetNth(&subchunk, &wpi->unknown_, 0) != WEBP_MUX_OK) goto Fail;
148         break;
149       default:
150         goto Fail;
151         break;
152     }
153     subchunk_size = ChunkDiskSize(&subchunk);
154     bytes += subchunk_size;
155     size -= subchunk_size;
156   }
157   if (wpi->is_partial_) goto Fail;
158   return 1;
159 
160  Fail:
161   ChunkRelease(&subchunk);
162   return 0;
163 }
164 
165 //------------------------------------------------------------------------------
166 // Create a mux object from WebP-RIFF data.
167 
WebPMuxCreateInternal(const WebPData * bitstream,int copy_data,int version)168 WebPMux* WebPMuxCreateInternal(const WebPData* bitstream, int copy_data,
169                                int version) {
170   size_t riff_size;
171   uint32_t tag;
172   const uint8_t* end;
173   WebPMux* mux = NULL;
174   WebPMuxImage* wpi = NULL;
175   const uint8_t* data;
176   size_t size;
177   WebPChunk chunk;
178   ChunkInit(&chunk);
179 
180   // Sanity checks.
181   if (WEBP_ABI_IS_INCOMPATIBLE(version, WEBP_MUX_ABI_VERSION)) {
182     return NULL;  // version mismatch
183   }
184   if (bitstream == NULL) return NULL;
185 
186   data = bitstream->bytes;
187   size = bitstream->size;
188 
189   if (data == NULL) return NULL;
190   if (size < RIFF_HEADER_SIZE) return NULL;
191   if (GetLE32(data + 0) != MKFOURCC('R', 'I', 'F', 'F') ||
192       GetLE32(data + CHUNK_HEADER_SIZE) != MKFOURCC('W', 'E', 'B', 'P')) {
193     return NULL;
194   }
195 
196   mux = WebPMuxNew();
197   if (mux == NULL) return NULL;
198 
199   if (size < RIFF_HEADER_SIZE + TAG_SIZE) goto Err;
200 
201   tag = GetLE32(data + RIFF_HEADER_SIZE);
202   if (tag != kChunks[IDX_VP8].tag &&
203       tag != kChunks[IDX_VP8L].tag &&
204       tag != kChunks[IDX_VP8X].tag) {
205     goto Err;  // First chunk should be VP8, VP8L or VP8X.
206   }
207 
208   riff_size = SizeWithPadding(GetLE32(data + TAG_SIZE));
209   if (riff_size > MAX_CHUNK_PAYLOAD || riff_size > size) {
210     goto Err;
211   } else {
212     if (riff_size < size) {  // Redundant data after last chunk.
213       size = riff_size;  // To make sure we don't read any data beyond mux_size.
214     }
215   }
216 
217   end = data + size;
218   data += RIFF_HEADER_SIZE;
219   size -= RIFF_HEADER_SIZE;
220 
221   wpi = (WebPMuxImage*)WebPSafeMalloc(1ULL, sizeof(*wpi));
222   if (wpi == NULL) goto Err;
223   MuxImageInit(wpi);
224 
225   // Loop over chunks.
226   while (data != end) {
227     size_t data_size;
228     WebPChunkId id;
229     WebPChunk** chunk_list;
230     if (ChunkVerifyAndAssign(&chunk, data, size, riff_size,
231                              copy_data) != WEBP_MUX_OK) {
232       goto Err;
233     }
234     data_size = ChunkDiskSize(&chunk);
235     id = ChunkGetIdFromTag(chunk.tag_);
236     switch (id) {
237       case WEBP_CHUNK_ALPHA:
238         if (wpi->alpha_ != NULL) goto Err;  // Consecutive ALPH chunks.
239         if (ChunkSetNth(&chunk, &wpi->alpha_, 1) != WEBP_MUX_OK) goto Err;
240         wpi->is_partial_ = 1;  // Waiting for a VP8 chunk.
241         break;
242       case WEBP_CHUNK_IMAGE:
243         if (ChunkSetNth(&chunk, &wpi->img_, 1) != WEBP_MUX_OK) goto Err;
244         if (!MuxImageFinalize(wpi)) goto Err;
245         wpi->is_partial_ = 0;  // wpi is completely filled.
246  PushImage:
247         // Add this to mux->images_ list.
248         if (MuxImagePush(wpi, &mux->images_) != WEBP_MUX_OK) goto Err;
249         MuxImageInit(wpi);  // Reset for reading next image.
250         break;
251       case WEBP_CHUNK_ANMF:
252         if (wpi->is_partial_) goto Err;  // Previous wpi is still incomplete.
253         if (!MuxImageParse(&chunk, copy_data, wpi)) goto Err;
254         ChunkRelease(&chunk);
255         goto PushImage;
256         break;
257       default:  // A non-image chunk.
258         if (wpi->is_partial_) goto Err;  // Encountered a non-image chunk before
259                                          // getting all chunks of an image.
260         chunk_list = MuxGetChunkListFromId(mux, id);  // List to add this chunk.
261         if (ChunkSetNth(&chunk, chunk_list, 0) != WEBP_MUX_OK) goto Err;
262         if (id == WEBP_CHUNK_VP8X) {  // grab global specs
263           mux->canvas_width_ = GetLE24(data + 12) + 1;
264           mux->canvas_height_ = GetLE24(data + 15) + 1;
265         }
266         break;
267     }
268     data += data_size;
269     size -= data_size;
270     ChunkInit(&chunk);
271   }
272 
273   // Validate mux if complete.
274   if (MuxValidate(mux) != WEBP_MUX_OK) goto Err;
275 
276   MuxImageDelete(wpi);
277   return mux;  // All OK;
278 
279  Err:  // Something bad happened.
280   ChunkRelease(&chunk);
281   MuxImageDelete(wpi);
282   WebPMuxDelete(mux);
283   return NULL;
284 }
285 
286 //------------------------------------------------------------------------------
287 // Get API(s).
288 
289 // Validates that the given mux has a single image.
ValidateForSingleImage(const WebPMux * const mux)290 static WebPMuxError ValidateForSingleImage(const WebPMux* const mux) {
291   const int num_images = MuxImageCount(mux->images_, WEBP_CHUNK_IMAGE);
292   const int num_frames = MuxImageCount(mux->images_, WEBP_CHUNK_ANMF);
293 
294   if (num_images == 0) {
295     // No images in mux.
296     return WEBP_MUX_NOT_FOUND;
297   } else if (num_images == 1 && num_frames == 0) {
298     // Valid case (single image).
299     return WEBP_MUX_OK;
300   } else {
301     // Frame case OR an invalid mux.
302     return WEBP_MUX_INVALID_ARGUMENT;
303   }
304 }
305 
306 // Get the canvas width, height and flags after validating that VP8X/VP8/VP8L
307 // chunk and canvas size are valid.
MuxGetCanvasInfo(const WebPMux * const mux,int * width,int * height,uint32_t * flags)308 static WebPMuxError MuxGetCanvasInfo(const WebPMux* const mux,
309                                      int* width, int* height, uint32_t* flags) {
310   int w, h;
311   uint32_t f = 0;
312   WebPData data;
313   assert(mux != NULL);
314 
315   // Check if VP8X chunk is present.
316   if (MuxGet(mux, IDX_VP8X, 1, &data) == WEBP_MUX_OK) {
317     if (data.size < VP8X_CHUNK_SIZE) return WEBP_MUX_BAD_DATA;
318     f = GetLE32(data.bytes + 0);
319     w = GetLE24(data.bytes + 4) + 1;
320     h = GetLE24(data.bytes + 7) + 1;
321   } else {
322     const WebPMuxImage* const wpi = mux->images_;
323     // Grab user-forced canvas size as default.
324     w = mux->canvas_width_;
325     h = mux->canvas_height_;
326     if (w == 0 && h == 0 && ValidateForSingleImage(mux) == WEBP_MUX_OK) {
327       // single image and not forced canvas size => use dimension of first frame
328       assert(wpi != NULL);
329       w = wpi->width_;
330       h = wpi->height_;
331     }
332     if (wpi != NULL) {
333       if (wpi->has_alpha_) f |= ALPHA_FLAG;
334     }
335   }
336   if (w * (uint64_t)h >= MAX_IMAGE_AREA) return WEBP_MUX_BAD_DATA;
337 
338   if (width != NULL) *width = w;
339   if (height != NULL) *height = h;
340   if (flags != NULL) *flags = f;
341   return WEBP_MUX_OK;
342 }
343 
WebPMuxGetCanvasSize(const WebPMux * mux,int * width,int * height)344 WebPMuxError WebPMuxGetCanvasSize(const WebPMux* mux, int* width, int* height) {
345   if (mux == NULL || width == NULL || height == NULL) {
346     return WEBP_MUX_INVALID_ARGUMENT;
347   }
348   return MuxGetCanvasInfo(mux, width, height, NULL);
349 }
350 
WebPMuxGetFeatures(const WebPMux * mux,uint32_t * flags)351 WebPMuxError WebPMuxGetFeatures(const WebPMux* mux, uint32_t* flags) {
352   if (mux == NULL || flags == NULL) return WEBP_MUX_INVALID_ARGUMENT;
353   return MuxGetCanvasInfo(mux, NULL, NULL, flags);
354 }
355 
EmitVP8XChunk(uint8_t * const dst,int width,int height,uint32_t flags)356 static uint8_t* EmitVP8XChunk(uint8_t* const dst, int width,
357                               int height, uint32_t flags) {
358   const size_t vp8x_size = CHUNK_HEADER_SIZE + VP8X_CHUNK_SIZE;
359   assert(width >= 1 && height >= 1);
360   assert(width <= MAX_CANVAS_SIZE && height <= MAX_CANVAS_SIZE);
361   assert(width * (uint64_t)height < MAX_IMAGE_AREA);
362   PutLE32(dst, MKFOURCC('V', 'P', '8', 'X'));
363   PutLE32(dst + TAG_SIZE, VP8X_CHUNK_SIZE);
364   PutLE32(dst + CHUNK_HEADER_SIZE, flags);
365   PutLE24(dst + CHUNK_HEADER_SIZE + 4, width - 1);
366   PutLE24(dst + CHUNK_HEADER_SIZE + 7, height - 1);
367   return dst + vp8x_size;
368 }
369 
370 // Assemble a single image WebP bitstream from 'wpi'.
SynthesizeBitstream(const WebPMuxImage * const wpi,WebPData * const bitstream)371 static WebPMuxError SynthesizeBitstream(const WebPMuxImage* const wpi,
372                                         WebPData* const bitstream) {
373   uint8_t* dst;
374 
375   // Allocate data.
376   const int need_vp8x = (wpi->alpha_ != NULL);
377   const size_t vp8x_size = need_vp8x ? CHUNK_HEADER_SIZE + VP8X_CHUNK_SIZE : 0;
378   const size_t alpha_size = need_vp8x ? ChunkDiskSize(wpi->alpha_) : 0;
379   // Note: No need to output ANMF chunk for a single image.
380   const size_t size = RIFF_HEADER_SIZE + vp8x_size + alpha_size +
381                       ChunkDiskSize(wpi->img_);
382   uint8_t* const data = (uint8_t*)WebPSafeMalloc(1ULL, size);
383   if (data == NULL) return WEBP_MUX_MEMORY_ERROR;
384 
385   // Main RIFF header.
386   dst = MuxEmitRiffHeader(data, size);
387 
388   if (need_vp8x) {
389     dst = EmitVP8XChunk(dst, wpi->width_, wpi->height_, ALPHA_FLAG);  // VP8X.
390     dst = ChunkListEmit(wpi->alpha_, dst);       // ALPH.
391   }
392 
393   // Bitstream.
394   dst = ChunkListEmit(wpi->img_, dst);
395   assert(dst == data + size);
396 
397   // Output.
398   bitstream->bytes = data;
399   bitstream->size = size;
400   return WEBP_MUX_OK;
401 }
402 
WebPMuxGetChunk(const WebPMux * mux,const char fourcc[4],WebPData * chunk_data)403 WebPMuxError WebPMuxGetChunk(const WebPMux* mux, const char fourcc[4],
404                              WebPData* chunk_data) {
405   CHUNK_INDEX idx;
406   if (mux == NULL || fourcc == NULL || chunk_data == NULL) {
407     return WEBP_MUX_INVALID_ARGUMENT;
408   }
409   idx = ChunkGetIndexFromFourCC(fourcc);
410   if (IsWPI(kChunks[idx].id)) {     // An image chunk.
411     return WEBP_MUX_INVALID_ARGUMENT;
412   } else if (idx != IDX_UNKNOWN) {  // A known chunk type.
413     return MuxGet(mux, idx, 1, chunk_data);
414   } else {                          // An unknown chunk type.
415     const WebPChunk* const chunk =
416         ChunkSearchList(mux->unknown_, 1, ChunkGetTagFromFourCC(fourcc));
417     if (chunk == NULL) return WEBP_MUX_NOT_FOUND;
418     *chunk_data = chunk->data_;
419     return WEBP_MUX_OK;
420   }
421 }
422 
MuxGetImageInternal(const WebPMuxImage * const wpi,WebPMuxFrameInfo * const info)423 static WebPMuxError MuxGetImageInternal(const WebPMuxImage* const wpi,
424                                         WebPMuxFrameInfo* const info) {
425   // Set some defaults for unrelated fields.
426   info->x_offset = 0;
427   info->y_offset = 0;
428   info->duration = 1;
429   info->dispose_method = WEBP_MUX_DISPOSE_NONE;
430   info->blend_method = WEBP_MUX_BLEND;
431   // Extract data for related fields.
432   info->id = ChunkGetIdFromTag(wpi->img_->tag_);
433   return SynthesizeBitstream(wpi, &info->bitstream);
434 }
435 
MuxGetFrameInternal(const WebPMuxImage * const wpi,WebPMuxFrameInfo * const frame)436 static WebPMuxError MuxGetFrameInternal(const WebPMuxImage* const wpi,
437                                         WebPMuxFrameInfo* const frame) {
438   const int is_frame = (wpi->header_->tag_ == kChunks[IDX_ANMF].tag);
439   const WebPData* frame_data;
440   if (!is_frame) return WEBP_MUX_INVALID_ARGUMENT;
441   assert(wpi->header_ != NULL);  // Already checked by WebPMuxGetFrame().
442   // Get frame chunk.
443   frame_data = &wpi->header_->data_;
444   if (frame_data->size < kChunks[IDX_ANMF].size) return WEBP_MUX_BAD_DATA;
445   // Extract info.
446   frame->x_offset = 2 * GetLE24(frame_data->bytes + 0);
447   frame->y_offset = 2 * GetLE24(frame_data->bytes + 3);
448   {
449     const uint8_t bits = frame_data->bytes[15];
450     frame->duration = GetLE24(frame_data->bytes + 12);
451     frame->dispose_method =
452         (bits & 1) ? WEBP_MUX_DISPOSE_BACKGROUND : WEBP_MUX_DISPOSE_NONE;
453     frame->blend_method = (bits & 2) ? WEBP_MUX_NO_BLEND : WEBP_MUX_BLEND;
454   }
455   frame->id = ChunkGetIdFromTag(wpi->header_->tag_);
456   return SynthesizeBitstream(wpi, &frame->bitstream);
457 }
458 
WebPMuxGetFrame(const WebPMux * mux,uint32_t nth,WebPMuxFrameInfo * frame)459 WebPMuxError WebPMuxGetFrame(
460     const WebPMux* mux, uint32_t nth, WebPMuxFrameInfo* frame) {
461   WebPMuxError err;
462   WebPMuxImage* wpi;
463 
464   // Sanity checks.
465   if (mux == NULL || frame == NULL) {
466     return WEBP_MUX_INVALID_ARGUMENT;
467   }
468 
469   // Get the nth WebPMuxImage.
470   err = MuxImageGetNth((const WebPMuxImage**)&mux->images_, nth, &wpi);
471   if (err != WEBP_MUX_OK) return err;
472 
473   // Get frame info.
474   if (wpi->header_ == NULL) {
475     return MuxGetImageInternal(wpi, frame);
476   } else {
477     return MuxGetFrameInternal(wpi, frame);
478   }
479 }
480 
WebPMuxGetAnimationParams(const WebPMux * mux,WebPMuxAnimParams * params)481 WebPMuxError WebPMuxGetAnimationParams(const WebPMux* mux,
482                                        WebPMuxAnimParams* params) {
483   WebPData anim;
484   WebPMuxError err;
485 
486   if (mux == NULL || params == NULL) return WEBP_MUX_INVALID_ARGUMENT;
487 
488   err = MuxGet(mux, IDX_ANIM, 1, &anim);
489   if (err != WEBP_MUX_OK) return err;
490   if (anim.size < kChunks[WEBP_CHUNK_ANIM].size) return WEBP_MUX_BAD_DATA;
491   params->bgcolor = GetLE32(anim.bytes);
492   params->loop_count = GetLE16(anim.bytes + 4);
493 
494   return WEBP_MUX_OK;
495 }
496 
497 // Get chunk index from chunk id. Returns IDX_NIL if not found.
ChunkGetIndexFromId(WebPChunkId id)498 static CHUNK_INDEX ChunkGetIndexFromId(WebPChunkId id) {
499   int i;
500   for (i = 0; kChunks[i].id != WEBP_CHUNK_NIL; ++i) {
501     if (id == kChunks[i].id) return (CHUNK_INDEX)i;
502   }
503   return IDX_NIL;
504 }
505 
506 // Count number of chunks matching 'tag' in the 'chunk_list'.
507 // If tag == NIL_TAG, any tag will be matched.
CountChunks(const WebPChunk * const chunk_list,uint32_t tag)508 static int CountChunks(const WebPChunk* const chunk_list, uint32_t tag) {
509   int count = 0;
510   const WebPChunk* current;
511   for (current = chunk_list; current != NULL; current = current->next_) {
512     if (tag == NIL_TAG || current->tag_ == tag) {
513       count++;  // Count chunks whose tags match.
514     }
515   }
516   return count;
517 }
518 
WebPMuxNumChunks(const WebPMux * mux,WebPChunkId id,int * num_elements)519 WebPMuxError WebPMuxNumChunks(const WebPMux* mux,
520                               WebPChunkId id, int* num_elements) {
521   if (mux == NULL || num_elements == NULL) {
522     return WEBP_MUX_INVALID_ARGUMENT;
523   }
524 
525   if (IsWPI(id)) {
526     *num_elements = MuxImageCount(mux->images_, id);
527   } else {
528     WebPChunk* const* chunk_list = MuxGetChunkListFromId(mux, id);
529     const CHUNK_INDEX idx = ChunkGetIndexFromId(id);
530     *num_elements = CountChunks(*chunk_list, kChunks[idx].tag);
531   }
532 
533   return WEBP_MUX_OK;
534 }
535 
536 //------------------------------------------------------------------------------
537