wsi-dicom 0.7.0

Convert whole-slide imaging files to DICOM VL Whole Slide Microscopy
Documentation
//! Source-aware default selection for export options.

use wsi_rs::Slide;

use crate::error::Error;
use crate::export::{
    jpeg_baseline_route_frame_geometry, pixel_profile_from_raw_jpeg_tile, plan_lossless_j2k_frames,
    raw_jpeg_profile_can_passthrough, raw_rgb_passthrough_has_no_geometry_fallback,
    read_raw_jpeg_passthrough_tile, FrameRectGrid, JpegBaselineFrameLocation,
    LosslessJ2kPlanRequest,
};
use crate::options::{ExportOptions, TransferSyntax};
use crate::request::DefaultTransferSyntaxRequest;
use crate::routing::{j2k_route_tile_size, level_is_synthetic_downsample};
use crate::tile::optical_path_groups;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum J2kDefaultPassthroughStatus {
    NoJ2kSource,
    Eligible,
    Blocked,
}

/// Pick the default transfer syntax for a source.
///
/// JPEG and JPEG 2000-backed sources preserve native compressed frames when an
/// eligible passthrough route is visible. Other sources fall back to HTJ2K
/// Lossless RPCL.
pub fn default_transfer_syntax_for_source(
    request: DefaultTransferSyntaxRequest,
) -> Result<TransferSyntax, Error> {
    if request.tile_size == 0 {
        return Err(Error::InvalidOptions {
            reason: "tile_size must be greater than zero".into(),
        });
    }
    if request.max_levels == Some(0) {
        return Err(Error::InvalidOptions {
            reason: "max_levels must be greater than zero when provided".into(),
        });
    }
    let max_levels = request
        .max_levels
        .map(usize::try_from)
        .transpose()
        .map_err(|_| Error::Unsupported {
            reason: "max_levels exceeds platform addressable memory".into(),
        })?;
    let slide = Slide::open(&request.source_path).map_err(|source| Error::SourceOpen {
        path: request.source_path.clone(),
        message: source.to_string(),
    })?;
    let mut j2k_passthrough_eligible = false;
    let mut j2k_passthrough_blocked = false;

    for (scene_idx, scene) in slide.dataset().scenes.iter().enumerate() {
        for (series_idx, series) in scene.series.iter().enumerate() {
            let level_limit = max_levels
                .unwrap_or(series.levels.len())
                .min(series.levels.len());
            for (level_idx, level) in series.levels.iter().take(level_limit).enumerate() {
                let level_idx = u32::try_from(level_idx).map_err(|_| Error::Unsupported {
                    reason: "default transfer syntax level index exceeds u32".into(),
                })?;
                if request
                    .level_filter
                    .is_some_and(|requested_level| requested_level != level_idx)
                {
                    continue;
                }
                if level_is_synthetic_downsample(&slide, scene_idx, series_idx, level_idx)? {
                    continue;
                }
                for z in 0..series.axes.z {
                    for t in 0..series.axes.t {
                        for c in optical_path_groups(series.axes.c) {
                            let location = JpegBaselineFrameLocation {
                                scene_idx,
                                series_idx,
                                level_idx,
                                z,
                                c,
                                t,
                            };
                            if jpeg_baseline_passthrough_available_for_default(
                                &slide,
                                level,
                                location,
                                request.tile_size,
                            )? {
                                return Ok(TransferSyntax::JpegBaseline8Bit);
                            }
                            match j2k_passthrough_status_for_default(
                                &slide,
                                level,
                                location,
                                request.tile_size,
                            )? {
                                J2kDefaultPassthroughStatus::Eligible => {
                                    j2k_passthrough_eligible = true;
                                }
                                J2kDefaultPassthroughStatus::Blocked => {
                                    j2k_passthrough_blocked = true;
                                }
                                J2kDefaultPassthroughStatus::NoJ2kSource => {}
                            }
                        }
                    }
                }
            }
        }
    }

    if j2k_passthrough_eligible && !j2k_passthrough_blocked {
        Ok(TransferSyntax::Jpeg2000)
    } else {
        Ok(TransferSyntax::Htj2kLosslessRpcl)
    }
}

fn jpeg_baseline_passthrough_available_for_default(
    slide: &Slide,
    level: &wsi_rs::Level,
    location: JpegBaselineFrameLocation,
    fallback_tile_size: u32,
) -> Result<bool, Error> {
    let geometry =
        match jpeg_baseline_route_frame_geometry(slide, level, location, fallback_tile_size) {
            Ok(geometry) => geometry,
            Err(err @ Error::InvalidOptions { .. }) => return Err(err),
            Err(_) => return Ok(false),
        };
    let Some(raw) = read_raw_jpeg_passthrough_tile(slide, location, geometry, 0)? else {
        return Ok(false);
    };
    let Ok(profile) = pixel_profile_from_raw_jpeg_tile(&raw) else {
        return Ok(false);
    };
    Ok(raw_jpeg_profile_can_passthrough(
        profile,
        raw_rgb_passthrough_has_no_geometry_fallback(level, geometry),
    ))
}

fn j2k_passthrough_status_for_default(
    slide: &Slide,
    level: &wsi_rs::Level,
    location: JpegBaselineFrameLocation,
    fallback_tile_size: u32,
) -> Result<J2kDefaultPassthroughStatus, Error> {
    let options = ExportOptions {
        tile_size: fallback_tile_size,
        transfer_syntax: TransferSyntax::Jpeg2000,
        ..ExportOptions::default()
    };
    let tile_size = j2k_route_tile_size(&options, level)?;
    let (matrix_columns, matrix_rows) = level.dimensions;
    if matrix_columns == 0 || matrix_rows == 0 {
        return Ok(J2kDefaultPassthroughStatus::NoJ2kSource);
    }
    let planned = plan_lossless_j2k_frames(
        slide,
        LosslessJ2kPlanRequest {
            location,
            start_row: 0,
            row_count: 1,
            start_col: 0,
            tile_count: 1,
            grid: FrameRectGrid {
                matrix_columns,
                matrix_rows,
                frame_columns: tile_size,
                frame_rows: tile_size,
            },
            transfer_syntax: TransferSyntax::Jpeg2000,
            allow_passthrough_probe: true,
        },
    )?;
    if planned.iter().any(|frame| frame.has_passthrough()) {
        return Ok(J2kDefaultPassthroughStatus::Eligible);
    }
    if planned.iter().any(|frame| frame.has_j2k_source()) {
        return Ok(J2kDefaultPassthroughStatus::Blocked);
    }
    Ok(J2kDefaultPassthroughStatus::NoJ2kSource)
}