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