wsi-dicom 0.2.0

DICOM whole-slide export layer for statumen datasets
Documentation
//! Encode route selection and fallback policy.

use std::path::Path;

use signinum_core::CompressedTransferSyntax;
use statumen::{LevelSourceKind, Slide, TileLayout};

use crate::error::WsiDicomError;
use crate::options::{DicomExportOptions, EncodeBackendPreference, TransferSyntax};
use crate::tile::PixelProfile;

pub(crate) fn j2k_route_tile_size(
    options: &DicomExportOptions,
    level: &statumen::Level,
) -> Result<u32, WsiDicomError> {
    if options.transfer_syntax.is_jpeg2000_passthrough_only() {
        let native_square = match level.tile_layout {
            TileLayout::Regular {
                tile_width,
                tile_height,
                ..
            }
            | TileLayout::WholeLevel {
                virtual_tile_width: tile_width,
                virtual_tile_height: tile_height,
                ..
            } if tile_width == tile_height && tile_width > 0 => Some(tile_width),
            TileLayout::Regular { .. }
            | TileLayout::WholeLevel { .. }
            | TileLayout::Irregular { .. } => None,
        };
        if let Some(tile_size) = native_square {
            return Ok(tile_size);
        }
    }
    if options.tile_size == 0 {
        return Err(WsiDicomError::InvalidOptions {
            reason: "tile_size must be greater than zero".into(),
        });
    }
    Ok(options.tile_size)
}

pub(crate) fn j2k_encode_transfer_syntax(transfer_syntax: TransferSyntax) -> TransferSyntax {
    if transfer_syntax == TransferSyntax::Jpeg2000 {
        TransferSyntax::Jpeg2000Lossless
    } else {
        transfer_syntax
    }
}

pub(crate) fn j2k_encode_backend(
    transfer_syntax: TransferSyntax,
    requested_backend: EncodeBackendPreference,
) -> EncodeBackendPreference {
    if transfer_syntax == TransferSyntax::Jpeg2000 {
        EncodeBackendPreference::CpuOnly
    } else {
        requested_backend
    }
}

pub(crate) fn j2k_encoded_lossless_profile(
    profile: PixelProfile,
    transfer_syntax: TransferSyntax,
) -> PixelProfile {
    if matches!(
        transfer_syntax,
        TransferSyntax::Jpeg2000
            | TransferSyntax::Jpeg2000Lossless
            | TransferSyntax::Htj2kLossless
            | TransferSyntax::Htj2kLosslessRpcl
    ) && profile.components == 3
    {
        PixelProfile {
            components: profile.components,
            bits_allocated: profile.bits_allocated,
            photometric_interpretation: "YBR_RCT",
        }
    } else {
        profile
    }
}

#[cfg(all(feature = "metal", target_os = "macos"))]
pub(crate) fn transfer_syntax_from_uid(uid: &str) -> Option<TransferSyntax> {
    [
        TransferSyntax::JpegBaseline8Bit,
        TransferSyntax::Jpeg2000,
        TransferSyntax::Jpeg2000,
        TransferSyntax::Jpeg2000Lossless,
        TransferSyntax::Htj2kLossless,
        TransferSyntax::Htj2kLosslessRpcl,
        TransferSyntax::ExplicitVrLittleEndian,
    ]
    .into_iter()
    .find(|transfer_syntax| transfer_syntax.uid() == uid)
}

pub(crate) fn source_path_has_extension(path: &Path, ext: &str) -> bool {
    path.extension()
        .and_then(|value| value.to_str())
        .is_some_and(|extension| extension.eq_ignore_ascii_case(ext))
}

pub(crate) fn j2k_family_passthrough_probe_allowed(
    source_path: &Path,
    transfer_syntax: TransferSyntax,
) -> bool {
    match transfer_syntax {
        TransferSyntax::Jpeg2000 | TransferSyntax::Jpeg2000Lossless => true,
        TransferSyntax::Htj2kLossless | TransferSyntax::Htj2kLosslessRpcl => {
            source_path_has_extension(source_path, "dcm")
        }
        _ => false,
    }
}

pub(crate) fn level_is_synthetic_downsample(
    slide: &Slide,
    scene_idx: usize,
    series_idx: usize,
    level_idx: u32,
) -> Result<bool, WsiDicomError> {
    slide
        .level_source_kind(scene_idx, series_idx, level_idx)
        .map(|kind| kind == LevelSourceKind::SyntheticDownsample)
        .map_err(|err| WsiDicomError::SlideRead {
            message: format!("failed to inspect level source kind: {err}"),
        })
}

pub(crate) fn required_passthrough_syntax(
    transfer_syntax: TransferSyntax,
    candidate_syntax: CompressedTransferSyntax,
) -> Option<CompressedTransferSyntax> {
    match transfer_syntax {
        TransferSyntax::Jpeg2000 => match candidate_syntax {
            CompressedTransferSyntax::Jpeg2000Lossless
            | CompressedTransferSyntax::Jpeg2000Lossy => Some(candidate_syntax),
            _ => None,
        },
        TransferSyntax::Jpeg2000Lossless => Some(CompressedTransferSyntax::Jpeg2000Lossless),
        TransferSyntax::Htj2kLossless => Some(CompressedTransferSyntax::HtJpeg2000Lossless),
        TransferSyntax::Htj2kLosslessRpcl => Some(CompressedTransferSyntax::HtJpeg2000Lossless),
        TransferSyntax::JpegBaseline8Bit | TransferSyntax::ExplicitVrLittleEndian => None,
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::*;

    #[test]
    fn htj2k_passthrough_probe_is_limited_to_dicom_sources() {
        assert!(j2k_family_passthrough_probe_allowed(
            Path::new("source.svs"),
            TransferSyntax::Jpeg2000
        ));
        assert!(j2k_family_passthrough_probe_allowed(
            Path::new("source.svs"),
            TransferSyntax::Jpeg2000Lossless
        ));
        assert!(j2k_family_passthrough_probe_allowed(
            Path::new("source.dcm"),
            TransferSyntax::Htj2kLosslessRpcl
        ));
        assert!(j2k_family_passthrough_probe_allowed(
            Path::new("source.DCM"),
            TransferSyntax::Htj2kLossless
        ));
        assert!(!j2k_family_passthrough_probe_allowed(
            Path::new("source.svs"),
            TransferSyntax::Htj2kLosslessRpcl
        ));
        assert!(!j2k_family_passthrough_probe_allowed(
            Path::new("source.ndpi"),
            TransferSyntax::Htj2kLosslessRpcl
        ));
    }
}