use super::*;
pub(super) fn frame_bytes_to_rgb_tile(
frame_bytes: &[u8],
width: u32,
height: u32,
samples_per_pixel: u16,
planar_configuration: u16,
photometric_interpretation: &str,
) -> Result<CpuTile, WsiError> {
let pixel_count = (width as usize)
.checked_mul(height as usize)
.ok_or_else(|| WsiError::DisplayConversion("DICOM frame dimensions overflow".into()))?;
let rgb = match (samples_per_pixel, photometric_interpretation) {
(3, "RGB") if planar_configuration == 0 => {
let expected = pixel_count.checked_mul(3).ok_or_else(|| {
WsiError::DisplayConversion("DICOM RGB frame size overflow".into())
})?;
if frame_bytes.len() != expected {
return Err(WsiError::DisplayConversion(format!(
"DICOM RGB frame has {} bytes, expected {expected}",
frame_bytes.len()
)));
}
frame_bytes.to_vec()
}
(3, "RGB") if planar_configuration == 1 => {
let expected = pixel_count.checked_mul(3).ok_or_else(|| {
WsiError::DisplayConversion("DICOM planar RGB frame size overflow".into())
})?;
if frame_bytes.len() != expected {
return Err(WsiError::DisplayConversion(format!(
"DICOM planar RGB frame has {} bytes, expected {expected}",
frame_bytes.len()
)));
}
let (r_plane, rest) = frame_bytes.split_at(pixel_count);
let (g_plane, b_plane) = rest.split_at(pixel_count);
let mut rgb = vec![0; expected];
for idx in 0..pixel_count {
let offset = idx * 3;
rgb[offset] = r_plane[idx];
rgb[offset + 1] = g_plane[idx];
rgb[offset + 2] = b_plane[idx];
}
rgb
}
(1, "MONOCHROME1" | "MONOCHROME2") => {
if frame_bytes.len() != pixel_count {
return Err(WsiError::DisplayConversion(format!(
"DICOM monochrome frame has {} bytes, expected {pixel_count}",
frame_bytes.len()
)));
}
let mut rgb = Vec::with_capacity(pixel_count * 3);
for &gray in frame_bytes {
rgb.extend_from_slice(&[gray, gray, gray]);
}
rgb
}
_ => {
return Err(WsiError::DisplayConversion(format!(
"unsupported DICOM pixel format: samples_per_pixel={samples_per_pixel}, photometric={photometric_interpretation}, planar_configuration={planar_configuration}"
)));
}
};
CpuTile::new(
width,
height,
3,
ColorSpace::Rgb,
CpuTileLayout::Interleaved,
CpuTileData::u8(rgb),
)
}
pub(super) fn decode_rle_lossless_frame(
frame_bytes: &[u8],
width: u32,
height: u32,
samples_per_pixel: u16,
photometric_interpretation: &str,
) -> Result<CpuTile, WsiError> {
if frame_bytes.len() < 64 {
return Err(WsiError::DisplayConversion(
"DICOM RLE frame is shorter than its 64-byte header".into(),
));
}
let segment_count = u32::from_le_bytes(frame_bytes[0..4].try_into().unwrap()) as usize;
if segment_count == 0 || segment_count > 15 {
return Err(WsiError::DisplayConversion(format!(
"DICOM RLE segment count {segment_count} is invalid"
)));
}
let expected_segments = samples_per_pixel as usize;
if segment_count < expected_segments {
return Err(WsiError::DisplayConversion(format!(
"DICOM RLE has {segment_count} segments, expected at least {expected_segments}"
)));
}
let pixel_count = (width as usize)
.checked_mul(height as usize)
.ok_or_else(|| WsiError::DisplayConversion("DICOM RLE dimensions overflow".into()))?;
let mut planes = Vec::with_capacity(expected_segments);
for segment in 0..expected_segments {
let offset_start = 4 + segment * 4;
let segment_start = u32::from_le_bytes(
frame_bytes[offset_start..offset_start + 4]
.try_into()
.unwrap(),
) as usize;
let segment_end = if segment + 1 < segment_count {
let next_offset_start = 4 + (segment + 1) * 4;
u32::from_le_bytes(
frame_bytes[next_offset_start..next_offset_start + 4]
.try_into()
.unwrap(),
) as usize
} else {
frame_bytes.len()
};
if segment_start < 64 || segment_start > segment_end || segment_end > frame_bytes.len() {
return Err(WsiError::DisplayConversion(format!(
"DICOM RLE segment {segment} has invalid byte range {segment_start}..{segment_end}"
)));
}
planes.push(decode_rle_segment(
&frame_bytes[segment_start..segment_end],
pixel_count,
)?);
}
let rgb = match (samples_per_pixel, photometric_interpretation) {
(3, "RGB") => {
let mut rgb = vec![0; pixel_count * 3];
for (idx, ((&red, &green), &blue)) in
planes[0].iter().zip(&planes[1]).zip(&planes[2]).enumerate()
{
let offset = idx * 3;
rgb[offset] = red;
rgb[offset + 1] = green;
rgb[offset + 2] = blue;
}
rgb
}
(1, "MONOCHROME1" | "MONOCHROME2") => {
let mut rgb = Vec::with_capacity(pixel_count * 3);
for &gray in &planes[0] {
rgb.extend_from_slice(&[gray, gray, gray]);
}
rgb
}
_ => {
return Err(WsiError::DisplayConversion(format!(
"unsupported DICOM RLE pixel format: samples_per_pixel={samples_per_pixel}, photometric={photometric_interpretation}"
)));
}
};
CpuTile::new(
width,
height,
3,
ColorSpace::Rgb,
CpuTileLayout::Interleaved,
CpuTileData::u8(rgb),
)
}
pub(super) fn decode_rle_segment(segment: &[u8], expected_len: usize) -> Result<Vec<u8>, WsiError> {
let mut output = Vec::with_capacity(expected_len);
let mut i = 0;
while i < segment.len() && output.len() < expected_len {
let n = segment[i] as i8;
i += 1;
match n {
0..=127 => {
let count = n as usize + 1;
let end = i.checked_add(count).ok_or_else(|| {
WsiError::DisplayConversion("DICOM RLE literal run overflow".into())
})?;
if end > segment.len() {
return Err(WsiError::DisplayConversion(
"DICOM RLE literal run exceeds segment length".into(),
));
}
output.extend_from_slice(&segment[i..end]);
i = end;
}
-127..=-1 => {
if i >= segment.len() {
return Err(WsiError::DisplayConversion(
"DICOM RLE repeat run missing value".into(),
));
}
let count = 1usize + (-n as usize);
output.extend(std::iter::repeat_n(segment[i], count));
i += 1;
}
-128 => {}
}
}
if output.len() != expected_len {
return Err(WsiError::DisplayConversion(format!(
"DICOM RLE segment decoded to {} bytes, expected {expected_len}",
output.len()
)));
}
Ok(output)
}
pub(super) fn crop_sample_buffer_rgb(
buffer: &CpuTile,
width: u32,
height: u32,
) -> Result<CpuTile, WsiError> {
if buffer.width == width && buffer.height == height {
return Ok(buffer.clone());
}
crop_rgb_interleaved_u8_buffer(buffer, 0, 0, width, height)
}
pub(super) fn crop_or_keep_sample_buffer_rgb(
buffer: CpuTile,
width: u32,
height: u32,
) -> Result<CpuTile, WsiError> {
if buffer.width == width && buffer.height == height {
return Ok(buffer);
}
crop_sample_buffer_rgb(&buffer, width, height)
}
pub(super) fn raw_compression_for_transfer_syntax(
transfer_syntax_uid: &str,
photometric_interpretation: &str,
) -> Result<Compression, WsiError> {
if transfer_syntax_uid == JPEG_TRANSFER_SYNTAX {
return Ok(Compression::Jpeg);
}
if JP2K_TRANSFER_SYNTAXES.contains(&transfer_syntax_uid) {
return Ok(if jp2k_photometric_is_ycbcr(photometric_interpretation) {
Compression::Jp2kYcbcr
} else {
Compression::Jp2kRgb
});
}
Err(WsiError::Unsupported {
reason: format!(
"raw compressed DICOM tile access requires JPEG Baseline or J2K/HTJ2K transfer syntax, got {transfer_syntax_uid}"
),
})
}
pub(super) fn jp2k_photometric_is_ycbcr(photometric_interpretation: &str) -> bool {
matches!(
photometric_interpretation,
"YBR_FULL" | "YBR_FULL_422" | "YBR_ICT" | "YBR_RCT"
)
}
pub(super) fn raw_photometric_interpretation(
samples_per_pixel: u16,
photometric_interpretation: &str,
) -> Result<EncodedTilePhotometricInterpretation, WsiError> {
match (samples_per_pixel, photometric_interpretation) {
(1, "MONOCHROME1" | "MONOCHROME2") => {
Ok(EncodedTilePhotometricInterpretation::Monochrome2)
}
(3, "RGB") => Ok(EncodedTilePhotometricInterpretation::Rgb),
(3, "YBR_FULL_422" | "YBR_FULL" | "YBR_ICT" | "YBR_RCT") => {
Ok(EncodedTilePhotometricInterpretation::YbrFull422)
}
(_, other) => Err(WsiError::Unsupported {
reason: format!(
"raw compressed DICOM tile access does not support photometric interpretation {other}"
),
}),
}
}
pub(super) fn trim_encapsulated_frame_padding(data: &mut Vec<u8>) {
if data.len() >= 3
&& data.last() == Some(&0)
&& data[data.len() - 3..data.len() - 1] == [0xFF, 0xD9]
{
data.pop();
}
}
pub(super) fn black_sample_buffer(width: u32, height: u32) -> CpuTile {
CpuTile::new(
width,
height,
3,
ColorSpace::Rgb,
CpuTileLayout::Interleaved,
CpuTileData::u8(vec![0; width as usize * height as usize * 3]),
)
.expect("black tile dimensions must match")
}