use super::{JxsError, JxsResult};
pub const SOC: u16 = 0xFF10;
pub const EOC: u16 = 0xFF11;
pub const PIH: u16 = 0xFF12;
pub const CDT: u16 = 0xFF13;
pub const WGT: u16 = 0xFF14;
pub const NLT: u16 = 0xFF15;
pub const CWD: u16 = 0xFF17;
pub const SLH: u16 = 0xFF19;
pub const CAP: u16 = 0xFF50;
pub const PROFILE_MAIN: u16 = 0x1500;
pub const PROFILE_LIGHT: u16 = 0x1100;
pub const PROFILE_HIGH: u16 = 0x2500;
#[derive(Debug, Clone)]
pub struct PicHeader {
pub width: u32,
pub height: u32,
pub slice_height: u32,
pub num_components: u8,
pub bit_depth: u8,
pub codestream_len: u32,
pub profile: u16,
pub level: u16,
}
#[derive(Debug, Clone)]
pub struct ComponentDesc {
pub bit_depth: u8,
pub sx: u8,
pub sy: u8,
}
#[derive(Debug, Clone)]
pub struct SliceHeader {
pub qp: u16,
pub data_offset: usize,
pub data_len: usize,
}
#[derive(Debug, Clone)]
pub struct JxsHeaders {
pub pih: PicHeader,
pub components: Vec<ComponentDesc>,
pub weights: Vec<u16>,
pub slices: Vec<SliceHeader>,
pub has_nlt: bool,
pub nlt_payload: Option<Vec<u8>>,
}
fn read_u16(data: &[u8], pos: usize) -> JxsResult<u16> {
if pos + 2 > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + 2,
have: data.len(),
});
}
Ok(u16::from_be_bytes([data[pos], data[pos + 1]]))
}
fn read_u32(data: &[u8], pos: usize) -> JxsResult<u32> {
if pos + 4 > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + 4,
have: data.len(),
});
}
Ok(u32::from_be_bytes([
data[pos],
data[pos + 1],
data[pos + 2],
data[pos + 3],
]))
}
fn read_u24(data: &[u8], pos: usize) -> JxsResult<u32> {
if pos + 3 > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + 3,
have: data.len(),
});
}
Ok(u32::from(data[pos]) << 16 | u32::from(data[pos + 1]) << 8 | u32::from(data[pos + 2]))
}
fn parse_pih_payload(payload: &[u8]) -> JxsResult<PicHeader> {
if payload.len() < 26 {
return Err(JxsError::InvalidHeader(format!(
"PIH payload too short: {} < 26 bytes",
payload.len()
)));
}
let codestream_len = read_u24(payload, 0)?;
let profile = read_u16(payload, 3)?;
let level = read_u16(payload, 5)?;
let width = u32::from(read_u16(payload, 7)?);
let height = u32::from(read_u16(payload, 9)?);
let slice_height = u32::from(read_u16(payload, 13)?);
let num_components = payload[15];
let bit_depth = payload[17];
if num_components == 0 {
return Err(JxsError::InvalidHeader(
"PIH Nc=0: at least one component required".to_string(),
));
}
if bit_depth == 0 || bit_depth > 16 {
return Err(JxsError::InvalidHeader(format!(
"PIH Ss={bit_depth}: bit depth must be 1–16"
)));
}
if width == 0 || height == 0 {
return Err(JxsError::InvalidHeader(format!(
"PIH frame size {width}x{height}: dimensions must be non-zero"
)));
}
let effective_slice_height = if slice_height == 0 {
height
} else {
slice_height
};
Ok(PicHeader {
width,
height,
slice_height: effective_slice_height,
num_components,
bit_depth,
codestream_len,
profile,
level,
})
}
fn parse_cdt_payload(payload: &[u8], num_components: u8) -> JxsResult<Vec<ComponentDesc>> {
let nc = num_components as usize;
if payload.len() < nc * 3 {
return Err(JxsError::InvalidHeader(format!(
"CDT payload too short for {nc} components: {} < {} bytes",
payload.len(),
nc * 3
)));
}
let mut components = Vec::with_capacity(nc);
for c in 0..nc {
let base = c * 3;
let bit_depth = payload[base];
let sx = if payload[base + 1] == 0 {
1
} else {
payload[base + 1]
};
let sy = if payload[base + 2] == 0 {
1
} else {
payload[base + 2]
};
components.push(ComponentDesc { bit_depth, sx, sy });
}
Ok(components)
}
fn parse_wgt_payload(payload: &[u8]) -> Vec<u16> {
let num_weights = payload.len() / 2;
let mut weights = Vec::with_capacity(num_weights);
for i in 0..num_weights {
let w = u16::from_be_bytes([payload[i * 2], payload[i * 2 + 1]]);
weights.push(w);
}
weights
}
fn parse_slh_payload(
payload: &[u8],
data_offset: usize,
data_len: usize,
) -> JxsResult<SliceHeader> {
if payload.len() < 2 {
return Err(JxsError::InvalidHeader(format!(
"SLH payload too short: {} < 2 bytes",
payload.len()
)));
}
let qp = read_u16(payload, 0)?;
Ok(SliceHeader {
qp,
data_offset,
data_len,
})
}
pub fn parse_headers(data: &[u8]) -> JxsResult<(JxsHeaders, usize)> {
if data.len() < 2 {
return Err(JxsError::TruncatedStream {
need: 2,
have: data.len(),
});
}
let first_marker = read_u16(data, 0)?;
if first_marker != SOC {
return Err(JxsError::InvalidMarker {
expected: SOC,
got: first_marker,
});
}
let mut pos = 2usize; let mut pih_opt: Option<PicHeader> = None;
let mut components: Vec<ComponentDesc> = Vec::new();
let mut weights: Vec<u16> = Vec::new();
let mut slices: Vec<SliceHeader> = Vec::new();
let mut has_nlt = false;
let mut nlt_payload: Option<Vec<u8>> = None;
loop {
if pos + 2 > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + 2,
have: data.len(),
});
}
let marker = read_u16(data, pos)?;
pos += 2;
match marker {
EOC => {
break;
}
PIH => {
let lp = read_u16(data, pos)? as usize;
if lp < 2 {
return Err(JxsError::InvalidHeader(format!(
"PIH Lp={lp} too small (minimum 2)"
)));
}
let payload_len = lp - 2;
pos += 2;
if pos + payload_len > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + payload_len,
have: data.len(),
});
}
let payload = &data[pos..pos + payload_len];
pih_opt = Some(parse_pih_payload(payload)?);
pos += payload_len;
}
CDT => {
let lp = read_u16(data, pos)? as usize;
let payload_len = lp.saturating_sub(2);
pos += 2;
if pos + payload_len > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + payload_len,
have: data.len(),
});
}
let payload = &data[pos..pos + payload_len];
let nc = pih_opt.as_ref().map(|p| p.num_components).unwrap_or(0);
if nc > 0 {
components = parse_cdt_payload(payload, nc)?;
}
pos += payload_len;
}
WGT => {
let lp = read_u16(data, pos)? as usize;
let payload_len = lp.saturating_sub(2);
pos += 2;
if pos + payload_len > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + payload_len,
have: data.len(),
});
}
weights = parse_wgt_payload(&data[pos..pos + payload_len]);
pos += payload_len;
}
NLT => {
let lp = read_u16(data, pos)? as usize;
let payload_len = lp.saturating_sub(2);
pos += 2;
if pos + payload_len > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + payload_len,
have: data.len(),
});
}
nlt_payload = Some(data[pos..pos + payload_len].to_vec());
pos += payload_len;
has_nlt = true;
}
SLH => {
let lslh = read_u16(data, pos)? as usize;
if lslh < 2 {
return Err(JxsError::InvalidHeader(format!(
"SLH Lslh={lslh} too small"
)));
}
let payload_len = lslh - 2;
pos += 2;
if pos + payload_len > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + payload_len,
have: data.len(),
});
}
let payload = &data[pos..pos + payload_len];
let data_start = pos + payload_len;
let slice_data_len = slice_data_len_to_trailing_eoc(data, data_start)
.unwrap_or_else(|| find_next_marker_offset(data, data_start));
let slh = parse_slh_payload(payload, data_start, slice_data_len)?;
slices.push(slh);
pos += payload_len;
pos += slice_data_len;
}
_ if (marker & 0xFF00) == 0xFF00 => {
if pos + 2 > data.len() {
return Err(JxsError::TruncatedStream {
need: pos + 2,
have: data.len(),
});
}
let lp = read_u16(data, pos)? as usize;
let payload_len = lp.saturating_sub(2);
pos += 2 + payload_len;
}
_ => {
return Err(JxsError::InvalidMarker {
expected: 0xFF00,
got: marker,
});
}
}
}
let pih = pih_opt.ok_or_else(|| JxsError::InvalidHeader("missing PIH marker".to_string()))?;
if components.is_empty() {
components = (0..pih.num_components)
.map(|_| ComponentDesc {
bit_depth: pih.bit_depth,
sx: 1,
sy: 1,
})
.collect();
}
Ok((
JxsHeaders {
pih,
components,
weights,
slices,
has_nlt,
nlt_payload,
},
pos,
))
}
fn slice_data_len_to_trailing_eoc(data: &[u8], start: usize) -> Option<usize> {
let n = data.len();
if n < 2 || start > n - 2 {
return None;
}
if data[n - 2] == 0xFF && data[n - 1] == (EOC & 0x00FF) as u8 {
Some((n - 2) - start)
} else {
None
}
}
fn find_next_marker_offset(data: &[u8], start: usize) -> usize {
let mut i = start;
while i + 1 < data.len() {
if data[i] == 0xFF && data[i + 1] != 0x00 {
return i - start;
}
i += 1;
}
data.len() - start
}
pub fn build_test_codestream(
width: u16,
height: u16,
slice_height: u16,
num_components: u8,
bit_depth: u8,
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&SOC.to_be_bytes());
buf.extend_from_slice(&PIH.to_be_bytes());
let lp: u16 = 28; buf.extend_from_slice(&lp.to_be_bytes());
buf.extend_from_slice(&[0x00, 0x00, 0x00]);
buf.extend_from_slice(&PROFILE_MAIN.to_be_bytes());
buf.extend_from_slice(&0x0000u16.to_be_bytes());
buf.extend_from_slice(&width.to_be_bytes());
buf.extend_from_slice(&height.to_be_bytes());
buf.extend_from_slice(&width.to_be_bytes());
buf.extend_from_slice(&slice_height.to_be_bytes());
buf.push(num_components);
buf.push(0x00);
buf.push(bit_depth);
buf.push(0x00);
buf.push(0x00);
buf.extend_from_slice(&[0x00, 0x00, 0x00]);
buf.push(0x00);
buf.push(0x00);
buf.push(0x00);
buf.extend_from_slice(&CDT.to_be_bytes());
let cdt_payload_len = num_components as usize * 3;
let cdt_lp = (cdt_payload_len + 2) as u16;
buf.extend_from_slice(&cdt_lp.to_be_bytes());
for _c in 0..num_components {
buf.push(bit_depth); buf.push(0x01); buf.push(0x01); }
buf.extend_from_slice(&EOC.to_be_bytes());
buf
}
pub fn build_test_codestream_with_nlt(
width: u16,
height: u16,
slice_height: u16,
num_components: u8,
bit_depth: u8,
t1: u16,
t2: u16,
) -> Vec<u8> {
let mut base = build_test_codestream(width, height, slice_height, num_components, bit_depth);
let eoc_pos = base.len() - 2;
base.truncate(eoc_pos);
base.extend_from_slice(&NLT.to_be_bytes());
let lnlt: u16 = 7;
base.extend_from_slice(&lnlt.to_be_bytes());
base.push(0x00);
base.extend_from_slice(&t1.to_be_bytes());
base.extend_from_slice(&t2.to_be_bytes());
base.extend_from_slice(&EOC.to_be_bytes());
base
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn soc_marker_value() {
assert_eq!(SOC, 0xFF10);
}
#[test]
fn eoc_marker_value() {
assert_eq!(EOC, 0xFF11);
}
#[test]
fn pih_marker_value() {
assert_eq!(PIH, 0xFF12);
}
#[test]
fn parse_minimal_test_codestream() {
let data = build_test_codestream(640, 480, 16, 3, 8);
let (headers, _end) = parse_headers(&data).expect("parse_headers");
assert_eq!(headers.pih.width, 640);
assert_eq!(headers.pih.height, 480);
assert_eq!(headers.pih.slice_height, 16);
assert_eq!(headers.pih.num_components, 3);
assert_eq!(headers.pih.bit_depth, 8);
assert_eq!(headers.components.len(), 3);
assert_eq!(headers.components[0].sx, 1);
assert_eq!(headers.components[0].sy, 1);
assert!(!headers.has_nlt);
assert!(headers.nlt_payload.is_none());
}
#[test]
fn parse_codestream_with_nlt_marker_captures_payload() {
let data = build_test_codestream_with_nlt(64, 64, 8, 1, 8, 64, 192);
let (headers, _end) = parse_headers(&data).expect("parse_headers");
assert!(headers.has_nlt);
assert!(headers.nlt_payload.is_some());
let payload = headers.nlt_payload.unwrap();
assert_eq!(payload[0], 0x00);
assert_eq!(u16::from_be_bytes([payload[1], payload[2]]), 64);
assert_eq!(u16::from_be_bytes([payload[3], payload[4]]), 192);
}
#[test]
fn parse_codestream_without_nlt_has_no_payload() {
let data = build_test_codestream(32, 32, 8, 1, 8);
let (headers, _) = parse_headers(&data).expect("parse_headers");
assert!(!headers.has_nlt);
assert!(headers.nlt_payload.is_none());
}
#[test]
fn parse_no_soc_returns_error() {
let data = [0x00u8, 0x00, 0x00, 0x00];
let result = parse_headers(&data);
assert!(result.is_err());
if let Err(JxsError::InvalidMarker { expected, got }) = result {
assert_eq!(expected, SOC);
assert_eq!(got, 0x0000);
} else {
panic!("expected InvalidMarker error");
}
}
#[test]
fn parse_truncated_after_soc_returns_error() {
let data = [0xFFu8, 0x10]; let result = parse_headers(&data);
assert!(result.is_err());
}
#[test]
fn build_test_codestream_single_component_10bit() {
let data = build_test_codestream(1920, 1080, 32, 1, 10);
let (headers, _) = parse_headers(&data).expect("parse");
assert_eq!(headers.pih.width, 1920);
assert_eq!(headers.pih.height, 1080);
assert_eq!(headers.pih.bit_depth, 10);
assert_eq!(headers.pih.num_components, 1);
}
#[test]
fn find_next_marker_at_start() {
let data = [0xFFu8, 0x11, 0x00, 0x00]; let len = find_next_marker_offset(&data, 0);
assert_eq!(len, 0);
}
#[test]
fn find_next_marker_not_found_returns_full_length() {
let data = [0x00u8, 0x01, 0x02, 0x03];
let len = find_next_marker_offset(&data, 0);
assert_eq!(len, 4);
}
}