1 // Copyright 2024 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 use super::libyuv; 16 use super::rgb_impl; 17 18 use crate::image::Plane; 19 use crate::image::YuvRange; 20 use crate::internal_utils::pixels::*; 21 use crate::internal_utils::*; 22 use crate::*; 23 24 #[repr(C)] 25 #[derive(Clone, Copy, Default, PartialEq)] 26 pub enum Format { 27 Rgb, 28 #[default] 29 Rgba, 30 Argb, 31 Bgr, 32 Bgra, 33 Abgr, 34 Rgb565, 35 Rgba1010102, // https://developer.android.com/reference/android/graphics/Bitmap.Config#RGBA_1010102 36 } 37 38 impl Format { offsets(&self) -> [usize; 4]39 pub fn offsets(&self) -> [usize; 4] { 40 match self { 41 Format::Rgb => [0, 1, 2, 0], 42 Format::Rgba => [0, 1, 2, 3], 43 Format::Argb => [1, 2, 3, 0], 44 Format::Bgr => [2, 1, 0, 0], 45 Format::Bgra => [2, 1, 0, 3], 46 Format::Abgr => [3, 2, 1, 0], 47 Format::Rgb565 | Format::Rgba1010102 => [0; 4], 48 } 49 } 50 r_offset(&self) -> usize51 pub fn r_offset(&self) -> usize { 52 self.offsets()[0] 53 } 54 g_offset(&self) -> usize55 pub fn g_offset(&self) -> usize { 56 self.offsets()[1] 57 } 58 b_offset(&self) -> usize59 pub fn b_offset(&self) -> usize { 60 self.offsets()[2] 61 } 62 alpha_offset(&self) -> usize63 pub fn alpha_offset(&self) -> usize { 64 self.offsets()[3] 65 } 66 has_alpha(&self) -> bool67 pub fn has_alpha(&self) -> bool { 68 !matches!(self, Format::Rgb | Format::Bgr | Format::Rgb565) 69 } 70 } 71 72 #[repr(C)] 73 #[derive(Clone, Copy, Default)] 74 pub enum ChromaUpsampling { 75 #[default] 76 Automatic, 77 Fastest, 78 BestQuality, 79 Nearest, 80 Bilinear, 81 } 82 83 impl ChromaUpsampling { nearest_neighbor_filter_allowed(&self) -> bool84 pub fn nearest_neighbor_filter_allowed(&self) -> bool { 85 // TODO: this function has to return different values based on whether libyuv is used. 86 !matches!(self, Self::Bilinear | Self::BestQuality) 87 } bilinear_or_better_filter_allowed(&self) -> bool88 pub fn bilinear_or_better_filter_allowed(&self) -> bool { 89 // TODO: this function has to return different values based on whether libyuv is used. 90 !matches!(self, Self::Nearest | Self::Fastest) 91 } 92 } 93 94 #[repr(C)] 95 #[derive(Clone, Copy, Default)] 96 pub enum ChromaDownsampling { 97 #[default] 98 Automatic, 99 Fastest, 100 BestQuality, 101 Average, 102 SharpYuv, 103 } 104 105 #[derive(Default)] 106 pub struct Image { 107 pub width: u32, 108 pub height: u32, 109 pub depth: u8, 110 pub format: Format, 111 pub chroma_upsampling: ChromaUpsampling, 112 pub chroma_downsampling: ChromaDownsampling, 113 pub premultiply_alpha: bool, 114 pub is_float: bool, 115 pub max_threads: i32, 116 pub pixels: Option<Pixels>, 117 pub row_bytes: u32, 118 } 119 120 #[derive(Debug, Default, PartialEq)] 121 pub enum AlphaMultiplyMode { 122 #[default] 123 NoOp, 124 Multiply, 125 UnMultiply, 126 } 127 128 impl Image { max_channel(&self) -> u16129 pub fn max_channel(&self) -> u16 { 130 ((1i32 << self.depth) - 1) as u16 131 } 132 max_channel_f(&self) -> f32133 pub fn max_channel_f(&self) -> f32 { 134 self.max_channel() as f32 135 } 136 create_from_yuv(image: &image::Image) -> Self137 pub fn create_from_yuv(image: &image::Image) -> Self { 138 Self { 139 width: image.width, 140 height: image.height, 141 depth: image.depth, 142 format: Format::Rgba, 143 chroma_upsampling: ChromaUpsampling::Automatic, 144 chroma_downsampling: ChromaDownsampling::Automatic, 145 premultiply_alpha: false, 146 is_float: false, 147 max_threads: 1, 148 pixels: None, 149 row_bytes: 0, 150 } 151 } 152 pixels(&mut self) -> *mut u8153 pub fn pixels(&mut self) -> *mut u8 { 154 if self.pixels.is_none() { 155 return std::ptr::null_mut(); 156 } 157 match self.pixels.unwrap_mut() { 158 Pixels::Pointer(ptr) => ptr.ptr_mut(), 159 Pixels::Pointer16(ptr) => ptr.ptr_mut() as *mut u8, 160 Pixels::Buffer(buffer) => buffer.as_mut_ptr(), 161 Pixels::Buffer16(buffer) => buffer.as_mut_ptr() as *mut u8, 162 } 163 } 164 row(&self, row: u32) -> AvifResult<&[u8]>165 pub fn row(&self, row: u32) -> AvifResult<&[u8]> { 166 self.pixels 167 .as_ref() 168 .ok_or(AvifError::NoContent)? 169 .slice(checked_mul!(row, self.row_bytes)?, self.row_bytes) 170 } 171 row_mut(&mut self, row: u32) -> AvifResult<&mut [u8]>172 pub fn row_mut(&mut self, row: u32) -> AvifResult<&mut [u8]> { 173 self.pixels 174 .as_mut() 175 .ok_or(AvifError::NoContent)? 176 .slice_mut(checked_mul!(row, self.row_bytes)?, self.row_bytes) 177 } 178 row16(&self, row: u32) -> AvifResult<&[u16]>179 pub fn row16(&self, row: u32) -> AvifResult<&[u16]> { 180 self.pixels 181 .as_ref() 182 .ok_or(AvifError::NoContent)? 183 .slice16(checked_mul!(row, self.row_bytes / 2)?, self.row_bytes / 2) 184 } 185 row16_mut(&mut self, row: u32) -> AvifResult<&mut [u16]>186 pub fn row16_mut(&mut self, row: u32) -> AvifResult<&mut [u16]> { 187 self.pixels 188 .as_mut() 189 .ok_or(AvifError::NoContent)? 190 .slice16_mut(checked_mul!(row, self.row_bytes / 2)?, self.row_bytes / 2) 191 } 192 allocate(&mut self) -> AvifResult<()>193 pub fn allocate(&mut self) -> AvifResult<()> { 194 let row_bytes = checked_mul!(self.width, self.pixel_size())?; 195 if self.channel_size() == 1 { 196 let buffer_size: usize = usize_from_u32(checked_mul!(row_bytes, self.height)?)?; 197 let buffer: Vec<u8> = vec![0; buffer_size]; 198 self.pixels = Some(Pixels::Buffer(buffer)); 199 } else { 200 let buffer_size: usize = usize_from_u32(checked_mul!(row_bytes / 2, self.height)?)?; 201 let buffer: Vec<u16> = vec![0; buffer_size]; 202 self.pixels = Some(Pixels::Buffer16(buffer)); 203 } 204 self.row_bytes = row_bytes; 205 Ok(()) 206 } 207 depth_valid(&self) -> bool208 pub fn depth_valid(&self) -> bool { 209 match (self.format, self.is_float, self.depth) { 210 (Format::Rgb565, false, 8) => true, 211 (Format::Rgb565, _, _) => false, 212 (_, true, 16) => true, // IEEE 754 half-precision binary16 213 (_, false, 8 | 10 | 12 | 16) => true, 214 _ => false, 215 } 216 } 217 has_alpha(&self) -> bool218 pub fn has_alpha(&self) -> bool { 219 match self.format { 220 Format::Rgba | Format::Bgra | Format::Argb | Format::Abgr | Format::Rgba1010102 => true, 221 Format::Rgb | Format::Bgr | Format::Rgb565 => false, 222 } 223 } 224 channel_size(&self) -> u32225 pub fn channel_size(&self) -> u32 { 226 match self.depth { 227 8 => 1, 228 10 | 12 | 16 => 2, 229 _ => panic!(), 230 } 231 } 232 channel_count(&self) -> u32233 pub fn channel_count(&self) -> u32 { 234 match self.format { 235 Format::Rgba | Format::Bgra | Format::Argb | Format::Abgr => 4, 236 Format::Rgb | Format::Bgr => 3, 237 Format::Rgb565 => 2, 238 Format::Rgba1010102 => 0, // This is never used. 239 } 240 } 241 pixel_size(&self) -> u32242 pub fn pixel_size(&self) -> u32 { 243 match self.format { 244 Format::Rgba | Format::Bgra | Format::Argb | Format::Abgr => self.channel_size() * 4, 245 Format::Rgb | Format::Bgr => self.channel_size() * 3, 246 Format::Rgb565 => 2, 247 Format::Rgba1010102 => 4, 248 } 249 } 250 convert_to_half_float(&mut self) -> AvifResult<()>251 fn convert_to_half_float(&mut self) -> AvifResult<()> { 252 let scale = 1.0 / self.max_channel_f(); 253 match libyuv::convert_to_half_float(self, scale) { 254 Ok(_) => return Ok(()), 255 Err(err) => { 256 if err != AvifError::NotImplemented { 257 return Err(err); 258 } 259 } 260 } 261 // This constant comes from libyuv. For details, see here: 262 // https://chromium.googlesource.com/libyuv/libyuv/+/2f87e9a7/source/row_common.cc#3537 263 let reinterpret_f32_as_u32 = |f: f32| u32::from_le_bytes(f.to_le_bytes()); 264 let multiplier = 1.925_93e-34 * scale; 265 for y in 0..self.height { 266 let row = self.row16_mut(y)?; 267 for pixel in row { 268 *pixel = (reinterpret_f32_as_u32((*pixel as f32) * multiplier) >> 13) as u16; 269 } 270 } 271 Ok(()) 272 } 273 convert_from_yuv(&mut self, image: &image::Image) -> AvifResult<()>274 pub fn convert_from_yuv(&mut self, image: &image::Image) -> AvifResult<()> { 275 if !image.has_plane(Plane::Y) || !image.depth_valid() { 276 return Err(AvifError::ReformatFailed); 277 } 278 if matches!( 279 image.matrix_coefficients, 280 MatrixCoefficients::Reserved 281 | MatrixCoefficients::Bt2020Cl 282 | MatrixCoefficients::Smpte2085 283 | MatrixCoefficients::ChromaDerivedCl 284 | MatrixCoefficients::Ictcp 285 ) { 286 return Err(AvifError::NotImplemented); 287 } 288 if image.matrix_coefficients == MatrixCoefficients::Ycgco 289 && image.yuv_range == YuvRange::Limited 290 { 291 return Err(AvifError::NotImplemented); 292 } 293 if matches!( 294 image.matrix_coefficients, 295 MatrixCoefficients::YcgcoRe | MatrixCoefficients::YcgcoRo 296 ) { 297 if image.yuv_range == YuvRange::Limited { 298 return Err(AvifError::NotImplemented); 299 } 300 let bit_offset = 301 if image.matrix_coefficients == MatrixCoefficients::YcgcoRe { 2 } else { 1 }; 302 if image.depth - bit_offset != self.depth { 303 return Err(AvifError::NotImplemented); 304 } 305 } 306 // Android MediaCodec maps all underlying YUV formats to PixelFormat::Yuv420. So do not 307 // perform this validation for Android MediaCodec. The libyuv wrapper will simply use Bt601 308 // coefficients for this color conversion. 309 #[cfg(not(feature = "android_mediacodec"))] 310 if image.matrix_coefficients == MatrixCoefficients::Identity 311 && !matches!(image.yuv_format, PixelFormat::Yuv444 | PixelFormat::Yuv400) 312 { 313 return Err(AvifError::NotImplemented); 314 } 315 316 let mut alpha_multiply_mode = AlphaMultiplyMode::NoOp; 317 if image.has_alpha() && self.has_alpha() { 318 if !image.alpha_premultiplied && self.premultiply_alpha { 319 alpha_multiply_mode = AlphaMultiplyMode::Multiply; 320 } else if image.alpha_premultiplied && !self.premultiply_alpha { 321 alpha_multiply_mode = AlphaMultiplyMode::UnMultiply; 322 } 323 } 324 325 let mut converted_with_libyuv: bool = false; 326 let mut alpha_reformatted_with_libyuv = false; 327 if alpha_multiply_mode == AlphaMultiplyMode::NoOp || self.has_alpha() { 328 match libyuv::yuv_to_rgb(image, self) { 329 Ok(alpha_reformatted) => { 330 alpha_reformatted_with_libyuv = alpha_reformatted; 331 converted_with_libyuv = true; 332 } 333 Err(err) => { 334 if err != AvifError::NotImplemented { 335 return Err(err); 336 } 337 } 338 } 339 } 340 if matches!( 341 image.yuv_format, 342 PixelFormat::AndroidNv12 | PixelFormat::AndroidNv21 343 ) | matches!(self.format, Format::Rgba1010102) 344 { 345 // These conversions are only supported via libyuv. 346 // TODO: b/362984605 - Handle alpha channel for these formats. 347 if converted_with_libyuv { 348 return Ok(()); 349 } else { 350 return Err(AvifError::NotImplemented); 351 } 352 } 353 if self.has_alpha() && !alpha_reformatted_with_libyuv { 354 if image.has_alpha() { 355 self.import_alpha_from(image)?; 356 } else { 357 self.set_opaque()?; 358 } 359 } 360 if !converted_with_libyuv { 361 let mut converted_by_fast_path = false; 362 if (matches!( 363 self.chroma_upsampling, 364 ChromaUpsampling::Nearest | ChromaUpsampling::Fastest 365 ) || matches!(image.yuv_format, PixelFormat::Yuv444 | PixelFormat::Yuv400)) 366 && (alpha_multiply_mode == AlphaMultiplyMode::NoOp || self.format.has_alpha()) 367 { 368 match rgb_impl::yuv_to_rgb_fast(image, self) { 369 Ok(_) => converted_by_fast_path = true, 370 Err(err) => { 371 if err != AvifError::NotImplemented { 372 return Err(err); 373 } 374 } 375 } 376 } 377 if !converted_by_fast_path { 378 rgb_impl::yuv_to_rgb_any(image, self, alpha_multiply_mode)?; 379 alpha_multiply_mode = AlphaMultiplyMode::NoOp; 380 } 381 } 382 match alpha_multiply_mode { 383 AlphaMultiplyMode::Multiply => self.premultiply_alpha()?, 384 AlphaMultiplyMode::UnMultiply => self.unpremultiply_alpha()?, 385 AlphaMultiplyMode::NoOp => {} 386 } 387 if self.is_float { 388 self.convert_to_half_float()?; 389 } 390 Ok(()) 391 } 392 shuffle_channels_to(self, format: Format) -> AvifResult<Image>393 pub fn shuffle_channels_to(self, format: Format) -> AvifResult<Image> { 394 if self.format == format { 395 return Ok(self); 396 } 397 if self.format == Format::Rgb565 || format == Format::Rgb565 { 398 return Err(AvifError::NotImplemented); 399 } 400 401 let mut dst = Image { 402 format, 403 pixels: None, 404 row_bytes: 0, 405 ..self 406 }; 407 dst.allocate()?; 408 409 let src_channel_count = self.channel_count(); 410 let dst_channel_count = dst.channel_count(); 411 let src_offsets = self.format.offsets(); 412 let dst_offsets = dst.format.offsets(); 413 let src_has_alpha = self.has_alpha(); 414 let dst_has_alpha = dst.has_alpha(); 415 let dst_max_channel = dst.max_channel(); 416 for y in 0..self.height { 417 if self.depth == 8 { 418 let src_row = self.row(y)?; 419 let dst_row = &mut dst.row_mut(y)?; 420 for x in 0..self.width { 421 let src_pixel_i = (src_channel_count * x) as usize; 422 let dst_pixel_i = (dst_channel_count * x) as usize; 423 for c in 0..3 { 424 dst_row[dst_pixel_i + dst_offsets[c]] = 425 src_row[src_pixel_i + src_offsets[c]]; 426 } 427 if dst_has_alpha { 428 dst_row[dst_pixel_i + dst_offsets[3]] = if src_has_alpha { 429 src_row[src_pixel_i + src_offsets[3]] 430 } else { 431 dst_max_channel as u8 432 }; 433 } 434 } 435 } else { 436 let src_row = self.row16(y)?; 437 let dst_row = &mut dst.row16_mut(y)?; 438 for x in 0..self.width { 439 let src_pixel_i = (src_channel_count * x) as usize; 440 let dst_pixel_i = (dst_channel_count * x) as usize; 441 for c in 0..3 { 442 dst_row[dst_pixel_i + dst_offsets[c]] = 443 src_row[src_pixel_i + src_offsets[c]]; 444 } 445 if dst_has_alpha { 446 dst_row[dst_pixel_i + dst_offsets[3]] = if src_has_alpha { 447 src_row[src_pixel_i + src_offsets[3]] 448 } else { 449 dst_max_channel 450 }; 451 } 452 } 453 } 454 } 455 Ok(dst) 456 } 457 } 458 459 #[cfg(test)] 460 mod tests { 461 use super::*; 462 463 use crate::decoder::Category; 464 use crate::image::YuvRange; 465 use crate::image::ALL_PLANES; 466 use crate::image::MAX_PLANE_COUNT; 467 468 use test_case::test_case; 469 use test_case::test_matrix; 470 471 const WIDTH: usize = 3; 472 const HEIGHT: usize = 3; 473 struct YuvParams { 474 width: u32, 475 height: u32, 476 depth: u8, 477 format: PixelFormat, 478 yuv_range: YuvRange, 479 color_primaries: ColorPrimaries, 480 matrix_coefficients: MatrixCoefficients, 481 planes: [[&'static [u16]; HEIGHT]; MAX_PLANE_COUNT], 482 } 483 484 const YUV_PARAMS: [YuvParams; 1] = [YuvParams { 485 width: WIDTH as u32, 486 height: HEIGHT as u32, 487 depth: 12, 488 format: PixelFormat::Yuv420, 489 yuv_range: YuvRange::Limited, 490 color_primaries: ColorPrimaries::Srgb, 491 matrix_coefficients: MatrixCoefficients::Bt709, 492 planes: [ 493 [ 494 &[1001, 1001, 1001], 495 &[1001, 1001, 1001], 496 &[1001, 1001, 1001], 497 ], 498 [&[1637, 1637], &[1637, 1637], &[1637, 1637]], 499 [&[3840, 3840], &[3840, 3840], &[3840, 3840]], 500 [&[0, 0, 2039], &[0, 2039, 4095], &[2039, 4095, 4095]], 501 ], 502 }]; 503 504 struct RgbParams { 505 params: ( 506 /*yuv_param_index:*/ usize, 507 /*format:*/ Format, 508 /*depth:*/ u8, 509 /*premultiply_alpha:*/ bool, 510 /*is_float:*/ bool, 511 ), 512 expected_rgba: [&'static [u16]; HEIGHT], 513 } 514 515 const RGB_PARAMS: [RgbParams; 5] = [ 516 RgbParams { 517 params: (0, Format::Rgba, 16, true, false), 518 expected_rgba: [ 519 &[0, 0, 0, 0, 0, 0, 0, 0, 32631, 1, 0, 32631], 520 &[0, 0, 0, 0, 32631, 1, 0, 32631, 65535, 2, 0, 65535], 521 &[32631, 1, 0, 32631, 65535, 2, 0, 65535, 65535, 2, 0, 65535], 522 ], 523 }, 524 RgbParams { 525 params: (0, Format::Rgba, 16, true, true), 526 expected_rgba: [ 527 &[0, 0, 0, 0, 0, 0, 0, 0, 14327, 256, 0, 14327], 528 &[0, 0, 0, 0, 14327, 256, 0, 14327, 15360, 512, 0, 15360], 529 &[ 530 14327, 256, 0, 14327, 15360, 512, 0, 15360, 15360, 512, 0, 15360, 531 ], 532 ], 533 }, 534 RgbParams { 535 params: (0, Format::Rgba, 16, false, true), 536 expected_rgba: [ 537 &[15360, 512, 0, 0, 15360, 512, 0, 0, 15360, 512, 0, 14327], 538 &[15360, 512, 0, 0, 15360, 512, 0, 14327, 15360, 512, 0, 15360], 539 &[ 540 15360, 512, 0, 14327, 15360, 512, 0, 15360, 15360, 512, 0, 15360, 541 ], 542 ], 543 }, 544 RgbParams { 545 params: (0, Format::Rgba, 16, false, false), 546 expected_rgba: [ 547 &[65535, 2, 0, 0, 65535, 2, 0, 0, 65535, 2, 0, 32631], 548 &[65535, 2, 0, 0, 65535, 2, 0, 32631, 65535, 2, 0, 65535], 549 &[65535, 2, 0, 32631, 65535, 2, 0, 65535, 65535, 2, 0, 65535], 550 ], 551 }, 552 RgbParams { 553 params: (0, Format::Bgra, 16, true, false), 554 expected_rgba: [ 555 &[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 32631, 32631], 556 &[0, 0, 0, 0, 0, 1, 32631, 32631, 0, 2, 65535, 65535], 557 &[0, 1, 32631, 32631, 0, 2, 65535, 65535, 0, 2, 65535, 65535], 558 ], 559 }, 560 ]; 561 562 #[test_matrix(0usize..5)] rgb_conversion(rgb_param_index: usize) -> AvifResult<()>563 fn rgb_conversion(rgb_param_index: usize) -> AvifResult<()> { 564 let rgb_params = &RGB_PARAMS[rgb_param_index]; 565 let yuv_params = &YUV_PARAMS[rgb_params.params.0]; 566 let mut image = image::Image { 567 width: yuv_params.width, 568 height: yuv_params.height, 569 depth: yuv_params.depth, 570 yuv_format: yuv_params.format, 571 color_primaries: yuv_params.color_primaries, 572 matrix_coefficients: yuv_params.matrix_coefficients, 573 yuv_range: yuv_params.yuv_range, 574 ..image::Image::default() 575 }; 576 image.allocate_planes(Category::Color)?; 577 image.allocate_planes(Category::Alpha)?; 578 let yuva_planes = &yuv_params.planes; 579 for plane in ALL_PLANES { 580 let plane_index = plane.to_usize(); 581 if yuva_planes[plane_index].is_empty() { 582 continue; 583 } 584 for y in 0..image.height(plane) { 585 let row16 = image.row16_mut(plane, y as u32)?; 586 assert_eq!(row16.len(), yuva_planes[plane_index][y].len()); 587 let dst = &mut row16[..]; 588 dst.copy_from_slice(yuva_planes[plane_index][y]); 589 } 590 } 591 592 let mut rgb = Image::create_from_yuv(&image); 593 assert_eq!(rgb.width, image.width); 594 assert_eq!(rgb.height, image.height); 595 assert_eq!(rgb.depth, image.depth); 596 597 rgb.format = rgb_params.params.1; 598 rgb.depth = rgb_params.params.2; 599 rgb.premultiply_alpha = rgb_params.params.3; 600 rgb.is_float = rgb_params.params.4; 601 602 rgb.allocate()?; 603 rgb.convert_from_yuv(&image)?; 604 605 for y in 0..rgb.height as usize { 606 let row16 = rgb.row16(y as u32)?; 607 assert_eq!(&row16[..], rgb_params.expected_rgba[y]); 608 } 609 Ok(()) 610 } 611 612 #[test_case(Format::Rgba, &[0, 1, 2, 3])] 613 #[test_case(Format::Abgr, &[3, 2, 1, 0])] 614 #[test_case(Format::Rgb, &[0, 1, 2])] shuffle_channels_to(format: Format, expected: &[u8])615 fn shuffle_channels_to(format: Format, expected: &[u8]) { 616 let image = Image { 617 width: 1, 618 height: 1, 619 depth: 8, 620 format: Format::Rgba, 621 pixels: Some(Pixels::Buffer(vec![0u8, 1, 2, 3])), 622 row_bytes: 4, 623 ..Default::default() 624 }; 625 assert_eq!( 626 image.shuffle_channels_to(format).unwrap().row(0).unwrap(), 627 expected 628 ); 629 } 630 } 631