use oxideav_core::{Error, Result};
pub const FRAME_IDENTIFIER: &[u8; 4] = b"icpf";
pub const PRORES_RAW_FRAME_IDENTIFIER: &[u8; 4] = b"aprh";
pub const ENCODER_IDENTIFIER: &[u8; 4] = b"oxav";
pub const CHROMA_FMT_422_CODE: u8 = 2;
pub const CHROMA_FMT_444_CODE: u8 = 3;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ChromaFormat {
Y422,
Y444,
}
impl ChromaFormat {
pub fn from_code(c: u8) -> Result<Self> {
match c {
CHROMA_FMT_422_CODE => Ok(Self::Y422),
CHROMA_FMT_444_CODE => Ok(Self::Y444),
other => Err(Error::unsupported(format!(
"prores: chroma_format {other} not supported"
))),
}
}
pub fn code(self) -> u8 {
match self {
Self::Y422 => CHROMA_FMT_422_CODE,
Self::Y444 => CHROMA_FMT_444_CODE,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Profile {
Proxy,
Lt,
Standard,
Hq,
Prores4444,
Prores4444Xq,
}
impl Profile {
pub fn fourcc(self) -> &'static [u8; 4] {
match self {
Profile::Proxy => b"apco",
Profile::Lt => b"apcs",
Profile::Standard => b"apcn",
Profile::Hq => b"apch",
Profile::Prores4444 => b"ap4h",
Profile::Prores4444Xq => b"ap4x",
}
}
pub fn chroma_format(self) -> ChromaFormat {
match self {
Profile::Proxy | Profile::Lt | Profile::Standard | Profile::Hq => ChromaFormat::Y422,
Profile::Prores4444 | Profile::Prores4444Xq => ChromaFormat::Y444,
}
}
pub fn default_quant_index(self) -> u8 {
match self {
Profile::Proxy => 8,
Profile::Lt => 6,
Profile::Standard => 4,
Profile::Hq => 2,
Profile::Prores4444 => 2,
Profile::Prores4444Xq => 1,
}
}
}
#[derive(Clone, Debug)]
pub struct FrameHeader {
pub frame_size: u32,
pub frame_header_size: u16,
pub bitstream_version: u8,
pub width: u16,
pub height: u16,
pub chroma_format: ChromaFormat,
pub interlace_mode: u8,
pub aspect_ratio_information: u8,
pub frame_rate_code: u8,
pub color_primaries: u8,
pub transfer_characteristic: u8,
pub matrix_coefficients: u8,
pub alpha_channel_type: u8,
pub luma_qmat: [u8; 64],
pub chroma_qmat: [u8; 64],
}
impl FrameHeader {
pub fn picture_count(&self) -> u32 {
if self.interlace_mode == 0 {
1
} else {
2
}
}
}
pub fn parse_frame(data: &[u8]) -> Result<(FrameHeader, &[u8])> {
if data.len() < 8 {
return Err(Error::invalid("prores: frame truncated (need 8 bytes)"));
}
let frame_size = u32::from_be_bytes(data[0..4].try_into().unwrap());
if &data[4..8] != FRAME_IDENTIFIER {
if &data[4..8] == PRORES_RAW_FRAME_IDENTIFIER {
return Err(Error::unsupported(
"prores: ProRes RAW sample ('aprh' marker) is not decodable — \
ProRes RAW is a separate Apple format (single-plane Bayer/CFA \
sensor data) outside the scope of SMPTE RDD 36",
));
}
return Err(Error::invalid("prores: frame magic mismatch (not 'icpf')"));
}
if (frame_size as usize) > data.len() {
return Err(Error::invalid(
"prores: frame_size exceeds available buffer",
));
}
if (frame_size as usize) < 8 {
return Err(Error::invalid(
"prores: frame_size below the 8-byte size+magic prefix",
));
}
let frame_data = &data[..frame_size as usize];
let after_magic = &frame_data[8..];
let (fh, after_fh) = parse_frame_header(after_magic)?;
Ok((fh, after_fh))
}
pub fn parse_frame_header(data: &[u8]) -> Result<(FrameHeader, &[u8])> {
if data.len() < 20 {
return Err(Error::invalid("prores: frame header truncated"));
}
let frame_header_size = u16::from_be_bytes(data[0..2].try_into().unwrap());
if (frame_header_size as usize) < 20 || (frame_header_size as usize) > data.len() {
return Err(Error::invalid("prores: bad frame_header_size"));
}
let _reserved = data[2];
let bitstream_version = data[3];
if bitstream_version > 1 {
return Err(Error::unsupported(format!(
"prores: unsupported bitstream_version {bitstream_version} \
(RDD 36 specifies versions 0 and 1)"
)));
}
let _enc_id = &data[4..8];
let width = u16::from_be_bytes(data[8..10].try_into().unwrap());
let height = u16::from_be_bytes(data[10..12].try_into().unwrap());
let b12 = data[12];
let chroma_code = (b12 >> 6) & 0x3;
let interlace_mode = (b12 >> 2) & 0x3;
let chroma_format = ChromaFormat::from_code(chroma_code)?;
if interlace_mode == 3 {
return Err(Error::invalid(
"prores: interlace_mode 3 is reserved (RDD 36 §6.1.1 Table 2)",
));
}
let b13 = data[13];
let aspect_ratio_information = (b13 >> 4) & 0xF;
let frame_rate_code = b13 & 0xF;
let color_primaries = data[14];
let transfer_characteristic = data[15];
let matrix_coefficients = data[16];
let b17 = data[17];
let alpha_channel_type = b17 & 0xF;
if bitstream_version == 0 {
if chroma_format != ChromaFormat::Y422 {
return Err(Error::invalid(format!(
"prores: bitstream_version 0 requires chroma_format=2 (4:2:2), got code {chroma_code} \
(RDD 36 §6.4)"
)));
}
if alpha_channel_type != 0 {
return Err(Error::invalid(format!(
"prores: bitstream_version 0 requires alpha_channel_type=0, got {alpha_channel_type} \
(RDD 36 §6.4)"
)));
}
}
let b19 = data[19];
let load_luma = (b19 >> 1) & 1;
let load_chroma = b19 & 1;
let mut luma_qmat = [4u8; 64];
let mut chroma_qmat = [4u8; 64];
let mut cursor = 20usize;
if load_luma == 1 {
if data.len() < cursor + 64 {
return Err(Error::invalid("prores: luma_qmat truncated"));
}
luma_qmat.copy_from_slice(&data[cursor..cursor + 64]);
cursor += 64;
if let Some(&bad) = luma_qmat.iter().find(|&&w| !(2..=63).contains(&w)) {
return Err(Error::invalid(format!(
"prores: luma_quantization_matrix entry {bad} out of range 2..=63 \
(RDD 36 §6.1.1)"
)));
}
}
if load_chroma == 1 {
if data.len() < cursor + 64 {
return Err(Error::invalid("prores: chroma_qmat truncated"));
}
chroma_qmat.copy_from_slice(&data[cursor..cursor + 64]);
cursor += 64;
if let Some(&bad) = chroma_qmat.iter().find(|&&w| !(2..=63).contains(&w)) {
return Err(Error::invalid(format!(
"prores: chroma_quantization_matrix entry {bad} out of range 2..=63 \
(RDD 36 §6.1.1)"
)));
}
} else if load_luma == 1 {
chroma_qmat = luma_qmat;
}
if cursor > frame_header_size as usize {
return Err(Error::invalid(
"prores: frame header parser overran declared size",
));
}
Ok((
FrameHeader {
frame_size: 0,
frame_header_size,
bitstream_version,
width,
height,
chroma_format,
interlace_mode,
aspect_ratio_information,
frame_rate_code,
color_primaries,
transfer_characteristic,
matrix_coefficients,
alpha_channel_type,
luma_qmat,
chroma_qmat,
},
&data[frame_header_size as usize..],
))
}
#[derive(Clone, Copy, Debug, Default)]
pub struct FrameMeta {
pub aspect_ratio_information: u8,
pub frame_rate_code: u8,
pub color_primaries: u8,
pub transfer_characteristic: u8,
pub matrix_coefficients: u8,
}
impl FrameMeta {
pub fn unknown() -> Self {
Self::default()
}
pub fn is_unknown(self) -> bool {
self.aspect_ratio_information == 0
&& self.frame_rate_code == 0
&& self.color_primaries == 0
&& self.transfer_characteristic == 0
&& self.matrix_coefficients == 0
}
}
pub fn frame_rate_code_from_rational(r: oxideav_core::Rational) -> u8 {
if r.num <= 0 || r.den <= 0 {
return 0;
}
let num = r.num as i128;
let den = r.den as i128;
let candidates: &[(u8, i128, i128)] = &[
(1, 24_000, 1001),
(2, 24, 1),
(3, 25, 1),
(4, 30_000, 1001),
(5, 30, 1),
(6, 50, 1),
(7, 60_000, 1001),
(8, 60, 1),
(9, 100, 1),
(10, 120_000, 1001),
(11, 120, 1),
];
for &(code, n, d) in candidates {
if num * d == n * den {
return code;
}
}
0
}
#[allow(clippy::too_many_arguments)]
pub fn write_frame(
out: &mut Vec<u8>,
total_frame_size: u32,
width: u16,
height: u16,
chroma_format: ChromaFormat,
interlace_mode: u8,
luma_qmat: &[u8; 64],
chroma_qmat: &[u8; 64],
load_luma: bool,
load_chroma: bool,
) {
write_frame_with_meta(
out,
total_frame_size,
width,
height,
chroma_format,
interlace_mode,
luma_qmat,
chroma_qmat,
load_luma,
load_chroma,
0,
FrameMeta::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn write_frame_with_alpha(
out: &mut Vec<u8>,
total_frame_size: u32,
width: u16,
height: u16,
chroma_format: ChromaFormat,
interlace_mode: u8,
luma_qmat: &[u8; 64],
chroma_qmat: &[u8; 64],
load_luma: bool,
load_chroma: bool,
alpha_channel_type: u8,
) {
write_frame_with_meta(
out,
total_frame_size,
width,
height,
chroma_format,
interlace_mode,
luma_qmat,
chroma_qmat,
load_luma,
load_chroma,
alpha_channel_type,
FrameMeta::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn write_frame_with_meta(
out: &mut Vec<u8>,
total_frame_size: u32,
width: u16,
height: u16,
chroma_format: ChromaFormat,
interlace_mode: u8,
luma_qmat: &[u8; 64],
chroma_qmat: &[u8; 64],
load_luma: bool,
load_chroma: bool,
alpha_channel_type: u8,
meta: FrameMeta,
) {
debug_assert!(alpha_channel_type <= 2);
debug_assert!(
interlace_mode <= 2,
"prores: interlace_mode {interlace_mode} is reserved (RDD 36 §6.1.1 Table 2)"
);
if load_luma {
debug_assert!(
luma_qmat.iter().all(|&w| (2..=63).contains(&w)),
"prores: luma_qmat entry out of range 2..=63 (RDD 36 §6.1.1)"
);
}
if load_chroma {
debug_assert!(
chroma_qmat.iter().all(|&w| (2..=63).contains(&w)),
"prores: chroma_qmat entry out of range 2..=63 (RDD 36 §6.1.1)"
);
}
out.extend_from_slice(&total_frame_size.to_be_bytes());
out.extend_from_slice(FRAME_IDENTIFIER);
let fh_size: u16 = 20 + if load_luma { 64 } else { 0 } + if load_chroma { 64 } else { 0 };
out.extend_from_slice(&fh_size.to_be_bytes());
out.push(0); let bitstream_version: u8 = if alpha_channel_type != 0 {
1
} else {
match chroma_format {
ChromaFormat::Y422 => 0,
ChromaFormat::Y444 => 1,
}
};
out.push(bitstream_version);
out.extend_from_slice(ENCODER_IDENTIFIER);
out.extend_from_slice(&width.to_be_bytes());
out.extend_from_slice(&height.to_be_bytes());
out.push((chroma_format.code() << 6) | ((interlace_mode & 0x3) << 2));
out.push(((meta.aspect_ratio_information & 0x0F) << 4) | (meta.frame_rate_code & 0x0F));
out.push(meta.color_primaries);
out.push(meta.transfer_characteristic);
out.push(meta.matrix_coefficients);
out.push(alpha_channel_type & 0x0F); out.push(0); let lb = ((load_luma as u8) << 1) | (load_chroma as u8);
out.push(lb);
if load_luma {
out.extend_from_slice(luma_qmat);
}
if load_chroma {
out.extend_from_slice(chroma_qmat);
}
}
#[derive(Clone, Debug)]
pub struct PictureHeader {
pub picture_header_size: u8,
pub picture_size: u32,
pub deprecated_number_of_slices: u16,
pub log2_desired_slice_size_in_mb: u8,
}
pub fn parse_picture_header(data: &[u8]) -> Result<(PictureHeader, &[u8])> {
if data.len() < 8 {
return Err(Error::invalid("prores: picture header truncated"));
}
let b0 = data[0];
let picture_header_size = (b0 >> 3) & 0x1F;
if picture_header_size < 8 {
return Err(Error::invalid("prores: picture_header_size < 8"));
}
let picture_size = u32::from_be_bytes(data[1..5].try_into().unwrap());
let deprecated_number_of_slices = u16::from_be_bytes(data[5..7].try_into().unwrap());
let b7 = data[7];
let log2_desired_slice_size_in_mb = (b7 >> 4) & 0x3;
if data.len() < picture_header_size as usize {
return Err(Error::invalid("prores: picture header overruns buffer"));
}
Ok((
PictureHeader {
picture_header_size,
picture_size,
deprecated_number_of_slices,
log2_desired_slice_size_in_mb,
},
&data[picture_header_size as usize..],
))
}
pub fn write_picture_header(
out: &mut Vec<u8>,
picture_size: u32,
deprecated_number_of_slices: u16,
log2_desired_slice_size_in_mb: u8,
) {
let picture_header_size: u8 = 8;
out.push(picture_header_size << 3);
out.extend_from_slice(&picture_size.to_be_bytes());
out.extend_from_slice(&deprecated_number_of_slices.to_be_bytes());
out.push((log2_desired_slice_size_in_mb & 0x3) << 4);
}
#[derive(Clone, Debug)]
pub struct SliceHeader {
pub slice_header_size: u8,
pub quantization_index: u8,
pub coded_size_of_y_data: u16,
pub coded_size_of_cb_data: u16,
pub coded_size_of_cr_data: Option<u16>,
}
pub fn parse_slice_header(data: &[u8], has_alpha: bool) -> Result<(SliceHeader, &[u8])> {
let min = if has_alpha { 8 } else { 6 };
if data.len() < min {
return Err(Error::invalid("prores: slice header truncated"));
}
let b0 = data[0];
let slice_header_size = (b0 >> 3) & 0x1F;
let quantization_index = data[1];
if !(1..=224).contains(&quantization_index) {
return Err(Error::invalid(
"prores: quantization_index out of range (1..=224)",
));
}
let coded_size_of_y_data = u16::from_be_bytes(data[2..4].try_into().unwrap());
let coded_size_of_cb_data = u16::from_be_bytes(data[4..6].try_into().unwrap());
let coded_size_of_cr_data = if has_alpha {
Some(u16::from_be_bytes(data[6..8].try_into().unwrap()))
} else {
None
};
let consumed = slice_header_size as usize;
if consumed < min {
return Err(Error::invalid("prores: slice_header_size < required"));
}
if data.len() < consumed {
return Err(Error::invalid("prores: slice header overruns buffer"));
}
Ok((
SliceHeader {
slice_header_size,
quantization_index,
coded_size_of_y_data,
coded_size_of_cb_data,
coded_size_of_cr_data,
},
&data[consumed..],
))
}
pub fn write_slice_header(
out: &mut Vec<u8>,
quantization_index: u8,
coded_size_of_y_data: u16,
coded_size_of_cb_data: u16,
coded_size_of_cr_data: Option<u16>,
) {
let slice_header_size: u8 = if coded_size_of_cr_data.is_some() {
8
} else {
6
};
out.push(slice_header_size << 3);
out.push(quantization_index);
out.extend_from_slice(&coded_size_of_y_data.to_be_bytes());
out.extend_from_slice(&coded_size_of_cb_data.to_be_bytes());
if let Some(cr) = coded_size_of_cr_data {
out.extend_from_slice(&cr.to_be_bytes());
}
}
pub fn compute_slice_sizes(width_in_mb: usize, log2_desired_slice_size_in_mb: u8) -> Vec<usize> {
let mut sizes = Vec::new();
let mut slice_size = 1usize << log2_desired_slice_size_in_mb;
let mut remaining = width_in_mb;
loop {
while remaining >= slice_size {
sizes.push(slice_size);
remaining -= slice_size;
}
slice_size /= 2;
if remaining == 0 {
break;
}
if slice_size == 0 {
break;
}
}
sizes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_frame_rejects_prores_raw_marker() {
let mut buf = Vec::new();
buf.extend_from_slice(&16u32.to_be_bytes());
buf.extend_from_slice(PRORES_RAW_FRAME_IDENTIFIER);
buf.extend_from_slice(&[0u8; 8]);
let err = parse_frame(&buf).expect_err("ProRes RAW must be rejected");
assert!(
err.to_string().contains("ProRes RAW"),
"error should name ProRes RAW, got: {err}"
);
}
#[test]
fn parse_frame_generic_magic_mismatch_is_not_reported_as_raw() {
let mut buf = Vec::new();
buf.extend_from_slice(&16u32.to_be_bytes());
buf.extend_from_slice(b"junk");
buf.extend_from_slice(&[0u8; 8]);
let err = parse_frame(&buf).expect_err("non-ProRes bytes must be rejected");
let msg = err.to_string();
assert!(msg.contains("magic mismatch"), "got: {msg}");
assert!(!msg.contains("ProRes RAW"), "got: {msg}");
}
#[test]
fn frame_roundtrip_422() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame(
&mut buf,
0,
128,
128,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.width, 128);
assert_eq!(fh.height, 128);
assert_eq!(fh.chroma_format, ChromaFormat::Y422);
assert_eq!(fh.bitstream_version, 0);
assert_eq!(fh.luma_qmat, [4u8; 64]);
assert_eq!(fh.chroma_qmat, [4u8; 64]);
}
#[test]
fn frame_roundtrip_444_with_qmats() {
let mut luma = [0u8; 64];
let mut chroma = [0u8; 64];
for i in 0..64 {
luma[i] = 2 + (i as u8 % 62); chroma[i] = 2 + ((i as u8 + 31) % 62); }
let mut buf = Vec::new();
write_frame(
&mut buf,
0,
64,
64,
ChromaFormat::Y444,
0,
&luma,
&chroma,
true,
true,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.chroma_format, ChromaFormat::Y444);
assert_eq!(fh.bitstream_version, 1);
assert_eq!(fh.luma_qmat, luma);
assert_eq!(fh.chroma_qmat, chroma);
}
#[test]
fn picture_header_roundtrip() {
let mut buf = Vec::new();
write_picture_header(&mut buf, 1234, 12, 3);
let (ph, _) = parse_picture_header(&buf).unwrap();
assert_eq!(ph.picture_header_size, 8);
assert_eq!(ph.picture_size, 1234);
assert_eq!(ph.deprecated_number_of_slices, 12);
assert_eq!(ph.log2_desired_slice_size_in_mb, 3);
}
#[test]
fn slice_header_roundtrip_no_alpha() {
let mut buf = Vec::new();
write_slice_header(&mut buf, 4, 100, 50, None);
let (sh, _) = parse_slice_header(&buf, false).unwrap();
assert_eq!(sh.slice_header_size, 6);
assert_eq!(sh.quantization_index, 4);
assert_eq!(sh.coded_size_of_y_data, 100);
assert_eq!(sh.coded_size_of_cb_data, 50);
assert!(sh.coded_size_of_cr_data.is_none());
}
#[test]
fn compute_slice_sizes_examples() {
assert_eq!(compute_slice_sizes(45, 3), vec![8, 8, 8, 8, 8, 4, 1]);
assert_eq!(compute_slice_sizes(8, 3), vec![8]);
assert_eq!(compute_slice_sizes(1, 3), vec![1]);
assert_eq!(compute_slice_sizes(5, 0), vec![1, 1, 1, 1, 1]);
}
#[test]
fn frame_rate_code_named_rates_match_spec_table_4() {
use oxideav_core::Rational;
let cases: &[(Rational, u8)] = &[
(Rational::new(24_000, 1001), 1),
(Rational::new(24, 1), 2),
(Rational::new(25, 1), 3),
(Rational::new(30_000, 1001), 4),
(Rational::new(30, 1), 5),
(Rational::new(50, 1), 6),
(Rational::new(60_000, 1001), 7),
(Rational::new(60, 1), 8),
(Rational::new(100, 1), 9),
(Rational::new(120_000, 1001), 10),
(Rational::new(120, 1), 11),
];
for &(r, expected) in cases {
let got = frame_rate_code_from_rational(r);
assert_eq!(
got, expected,
"rate {}/{} must map to {expected}",
r.num, r.den
);
}
}
#[test]
fn frame_rate_code_unnormalised_fractions_match() {
use oxideav_core::Rational;
assert_eq!(frame_rate_code_from_rational(Rational::new(60, 2)), 5);
assert_eq!(
frame_rate_code_from_rational(Rational::new(50_000, 1000)),
6
);
assert_eq!(
frame_rate_code_from_rational(Rational::new(48_000, 1001)),
0
);
}
#[test]
fn frame_rate_code_unknown_rates_map_to_zero() {
use oxideav_core::Rational;
assert_eq!(frame_rate_code_from_rational(Rational::new(48, 1)), 0);
assert_eq!(frame_rate_code_from_rational(Rational::new(90, 1)), 0);
assert_eq!(frame_rate_code_from_rational(Rational::new(0, 0)), 0);
assert_eq!(frame_rate_code_from_rational(Rational::new(-30, 1)), 0);
}
#[test]
fn frame_meta_is_unknown_helpers() {
assert!(FrameMeta::default().is_unknown());
assert!(FrameMeta::unknown().is_unknown());
let m = FrameMeta {
frame_rate_code: 5,
..FrameMeta::default()
};
assert!(!m.is_unknown());
}
#[test]
fn frame_with_meta_roundtrips_all_fields() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let meta = FrameMeta {
aspect_ratio_information: 3, frame_rate_code: 4, color_primaries: 9, transfer_characteristic: 16, matrix_coefficients: 9, };
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
1920,
1080,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
meta,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.aspect_ratio_information, meta.aspect_ratio_information);
assert_eq!(fh.frame_rate_code, meta.frame_rate_code);
assert_eq!(fh.color_primaries, meta.color_primaries);
assert_eq!(fh.transfer_characteristic, meta.transfer_characteristic);
assert_eq!(fh.matrix_coefficients, meta.matrix_coefficients);
}
#[test]
fn frame_with_alpha_back_compat_zeros_meta() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame_with_alpha(
&mut buf,
0,
64,
64,
ChromaFormat::Y444,
0,
&luma,
&chroma,
false,
false,
2, );
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.aspect_ratio_information, 0);
assert_eq!(fh.frame_rate_code, 0);
assert_eq!(fh.color_primaries, 0);
assert_eq!(fh.transfer_characteristic, 0);
assert_eq!(fh.matrix_coefficients, 0);
assert_eq!(fh.alpha_channel_type, 2);
}
}