wsi-dicom 0.7.0

Convert whole-slide imaging files to DICOM VL Whole Slide Microscopy
Documentation
use std::io::Write;
use std::path::Path;

use j2k_jpeg::{JpegBackend, JpegSamples, JpegSubsampling};

pub(crate) fn find_command_for_test(name: &str) -> Option<String> {
    let command = std::env::var_os("PATH")
        .and_then(|paths| {
            std::env::split_paths(&paths)
                .map(|path| path.join(name))
                .find(|path| path.is_file())
        })
        .or_else(|| {
            let staged = Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("target")
                .join("dicom3tools-mac")
                .join(name);
            staged.is_file().then_some(staged)
        })
        .map(|path| path.to_string_lossy().into_owned());
    if command.is_none() && external_tool_is_required(name) {
        panic!(
            "required external conformance tool `{name}` was not found; checked PATH and target/dicom3tools-mac"
        );
    }
    command
}

fn external_tool_is_required(name: &str) -> bool {
    std::env::var("WSI_DICOM_REQUIRED_EXTERNAL_TOOLS")
        .ok()
        .is_some_and(|required| external_tool_list_contains(&required, name))
}

fn external_tool_list_contains(required: &str, name: &str) -> bool {
    required
        .split(',')
        .map(str::trim)
        .any(|required_name| required_name == name)
}

pub(crate) fn read_binary_ppm_for_test(path: &Path) -> (u32, u32, Vec<u8>) {
    let bytes = std::fs::read(path).expect("read PPM");
    let mut cursor = 0usize;
    let magic = read_netpbm_token_for_test(&bytes, &mut cursor);
    assert_eq!(magic, "P6");
    let width = read_netpbm_token_for_test(&bytes, &mut cursor)
        .parse::<u32>()
        .expect("PPM width");
    let height = read_netpbm_token_for_test(&bytes, &mut cursor)
        .parse::<u32>()
        .expect("PPM height");
    let max_value = read_netpbm_token_for_test(&bytes, &mut cursor)
        .parse::<u32>()
        .expect("PPM max value");
    assert_eq!(max_value, 255);
    while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() {
        cursor += 1;
    }
    let expected_len = (width as usize) * (height as usize) * 3;
    assert_eq!(bytes.len() - cursor, expected_len);
    (width, height, bytes[cursor..].to_vec())
}

pub(crate) fn encode_test_jpeg(width: u32, height: u32, rgb: [u8; 3]) -> Vec<u8> {
    let pixels = vec![rgb; (width * height) as usize]
        .into_iter()
        .flatten()
        .collect::<Vec<_>>();
    j2k_jpeg::encode_jpeg_baseline(
        JpegSamples::Rgb8 {
            data: &pixels,
            width,
            height,
        },
        j2k_jpeg::JpegEncodeOptions {
            quality: 90,
            subsampling: JpegSubsampling::Ybr422,
            restart_interval: None,
            backend: JpegBackend::Cpu,
        },
    )
    .unwrap()
    .data
}

pub(crate) fn dicom_fragment_payload_without_padding(fragment: &[u8]) -> &[u8] {
    if fragment.len().is_multiple_of(2) && fragment.last() == Some(&0) {
        &fragment[..fragment.len() - 1]
    } else {
        fragment
    }
}

pub(crate) fn assert_htj2k_rpcl_codestream(codestream: &[u8]) {
    let cod_offset = codestream
        .windows(2)
        .position(|window| window == [0xFF, 0x52])
        .expect("COD marker");
    assert_eq!(codestream[cod_offset + 5], 0x02);
    assert!(codestream.windows(2).any(|window| window == [0xFF, 0x50]));
    assert!(codestream.windows(2).any(|window| window == [0xFF, 0x55]));
}

pub(crate) fn write_tiled_jpeg_tiff(
    path: &Path,
    width: u32,
    height: u32,
    tile_width: u32,
    tile_height: u32,
    tiles: &[Vec<u8>],
) {
    write_tiled_compressed_tiff(path, width, height, tile_width, tile_height, 7, 6, tiles);
}

pub(crate) fn write_tiled_jp2k_rgb_tiff(
    path: &Path,
    width: u32,
    height: u32,
    tile_width: u32,
    tile_height: u32,
    tiles: &[Vec<u8>],
) {
    write_tiled_compressed_tiff(
        path,
        width,
        height,
        tile_width,
        tile_height,
        33004,
        2,
        tiles,
    );
}

pub(crate) fn write_tiled_jp2k_ycbcr_tiff(
    path: &Path,
    width: u32,
    height: u32,
    tile_width: u32,
    tile_height: u32,
    tiles: &[Vec<u8>],
) {
    write_tiled_compressed_tiff(
        path,
        width,
        height,
        tile_width,
        tile_height,
        33005,
        6,
        tiles,
    );
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn write_tiled_compressed_tiff(
    path: &Path,
    width: u32,
    height: u32,
    tile_width: u32,
    tile_height: u32,
    compression: u16,
    photometric: u16,
    tiles: &[Vec<u8>],
) {
    let mut buf = Vec::new();
    buf.extend_from_slice(b"II");
    buf.extend_from_slice(&42u16.to_le_bytes());
    let first_ifd_pos = buf.len();
    buf.extend_from_slice(&0u32.to_le_bytes());

    let mut tile_offsets = Vec::with_capacity(tiles.len());
    let mut tile_byte_counts = Vec::with_capacity(tiles.len());
    for tile in tiles {
        tile_offsets.push(buf.len() as u32);
        tile_byte_counts.push(tile.len() as u32);
        buf.extend_from_slice(tile);
    }

    let tile_offsets_array_offset = buf.len() as u32;
    for value in &tile_offsets {
        buf.extend_from_slice(&value.to_le_bytes());
    }
    let tile_byte_counts_array_offset = buf.len() as u32;
    for value in &tile_byte_counts {
        buf.extend_from_slice(&value.to_le_bytes());
    }
    let x_resolution_offset = buf.len() as u32;
    buf.extend_from_slice(&40_000u32.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());
    let y_resolution_offset = buf.len() as u32;
    buf.extend_from_slice(&40_000u32.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());

    let ifd_offset = buf.len() as u32;
    buf[first_ifd_pos..first_ifd_pos + 4].copy_from_slice(&ifd_offset.to_le_bytes());
    let mut tags = vec![
        tiff_tag(256, 4, 1, width.to_le_bytes()),
        tiff_tag(257, 4, 1, height.to_le_bytes()),
        tiff_tag(258, 3, 1, tiff_short_value(8)),
        tiff_tag(259, 3, 1, tiff_short_value(compression)),
        tiff_tag(262, 3, 1, tiff_short_value(photometric)),
        tiff_tag(277, 3, 1, tiff_short_value(3)),
        tiff_tag(282, 5, 1, x_resolution_offset.to_le_bytes()),
        tiff_tag(283, 5, 1, y_resolution_offset.to_le_bytes()),
        tiff_tag(296, 3, 1, tiff_short_value(3)),
        tiff_tag(322, 4, 1, tile_width.to_le_bytes()),
        tiff_tag(323, 4, 1, tile_height.to_le_bytes()),
        tiff_tag(
            324,
            4,
            tile_offsets.len() as u32,
            if tile_offsets.len() == 1 {
                tile_offsets[0].to_le_bytes()
            } else {
                tile_offsets_array_offset.to_le_bytes()
            },
        ),
        tiff_tag(
            325,
            4,
            tile_byte_counts.len() as u32,
            if tile_byte_counts.len() == 1 {
                tile_byte_counts[0].to_le_bytes()
            } else {
                tile_byte_counts_array_offset.to_le_bytes()
            },
        ),
    ];
    tags.sort_by_key(|tag| tag.0);

    buf.extend_from_slice(&(tags.len() as u16).to_le_bytes());
    for (tag, typ, count, value) in tags {
        buf.extend_from_slice(&tag.to_le_bytes());
        buf.extend_from_slice(&typ.to_le_bytes());
        buf.extend_from_slice(&count.to_le_bytes());
        buf.extend_from_slice(&value);
    }
    buf.extend_from_slice(&0u32.to_le_bytes());

    let mut file = std::fs::File::create(path).unwrap();
    file.write_all(&buf).unwrap();
    file.flush().unwrap();
}

pub(crate) fn tiff_short_value(value: u16) -> [u8; 4] {
    let mut bytes = [0u8; 4];
    bytes[..2].copy_from_slice(&value.to_le_bytes());
    bytes
}

pub(crate) fn tiff_tag(tag: u16, typ: u16, count: u32, value: [u8; 4]) -> (u16, u16, u32, [u8; 4]) {
    (tag, typ, count, value)
}

fn read_netpbm_token_for_test(bytes: &[u8], cursor: &mut usize) -> String {
    loop {
        while *cursor < bytes.len() && bytes[*cursor].is_ascii_whitespace() {
            *cursor += 1;
        }
        if *cursor >= bytes.len() || bytes[*cursor] != b'#' {
            break;
        }
        while *cursor < bytes.len() && bytes[*cursor] != b'\n' {
            *cursor += 1;
        }
    }
    let start = *cursor;
    while *cursor < bytes.len() && !bytes[*cursor].is_ascii_whitespace() {
        *cursor += 1;
    }
    std::str::from_utf8(&bytes[start..*cursor])
        .expect("PPM token is UTF-8")
        .to_string()
}

#[cfg(test)]
mod tests {
    use super::{find_command_for_test, read_binary_ppm_for_test};

    #[test]
    fn read_binary_ppm_for_test_skips_comments_and_reads_pixels() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let path = tmp.path().join("sample.ppm");
        std::fs::write(&path, b"P6\n# comment\n2 1\n255\n\x01\x02\x03\x04\x05\x06")
            .expect("write PPM");

        let (width, height, pixels) = read_binary_ppm_for_test(&path);

        assert_eq!((width, height), (2, 1));
        assert_eq!(pixels, vec![1, 2, 3, 4, 5, 6]);
    }

    #[test]
    fn find_command_for_test_returns_none_for_missing_command() {
        assert!(find_command_for_test("wsi-dicom-command-that-should-not-exist").is_none());
    }

    #[test]
    fn external_tool_requirement_parser_matches_complete_names_only() {
        let required = "dciodvfy, opj_decompress";
        assert!(super::external_tool_list_contains(required, "dciodvfy"));
        assert!(super::external_tool_list_contains(
            required,
            "opj_decompress"
        ));
        assert!(!super::external_tool_list_contains(required, "dciod"));
    }

    #[test]
    fn encode_test_jpeg_emits_jpeg_baseline_bytes() {
        let jpeg = super::encode_test_jpeg(2, 1, [1, 2, 3]);

        assert!(jpeg.starts_with(&[0xFF, 0xD8]));
        assert!(jpeg.ends_with(&[0xFF, 0xD9]));
    }

    #[test]
    fn dicom_fragment_payload_without_padding_trims_only_even_zero_padding() {
        assert_eq!(
            super::dicom_fragment_payload_without_padding(&[1, 2, 3, 0]),
            &[1, 2, 3]
        );
        assert_eq!(
            super::dicom_fragment_payload_without_padding(&[1, 2, 3, 4]),
            &[1, 2, 3, 4]
        );
        assert_eq!(
            super::dicom_fragment_payload_without_padding(&[1, 2, 0]),
            &[1, 2, 0]
        );
    }
}