wsi-dicom 0.7.0

Convert whole-slide imaging files to DICOM VL Whole Slide Microscopy
Documentation
use std::time::Duration;

use j2k_core::CompressedTransferSyntax;
use wsi_rs::{Compression, Slide};

use super::frame_region::{FrameRectGrid, FrameRectOverflowReasons, OutputFrameRect};
use super::j2k_policy::{j2k_passthrough_frame, j2k_raw_frame_syntax_and_profile};
use super::jpeg_retile::{read_raw_jpeg_retile_display_tile, RawJpegRetileProbe};
use super::{j2k_direct_htj2k, jpeg_direct_htj2k};
use crate::coordinate::InstanceCoordinate;
use crate::error::Error;
use crate::options::TransferSyntax;
use crate::report::JpegRetileRejectionReason;
use crate::tile::PixelProfile;

pub(crate) struct LosslessJ2kPlannedFrame {
    pub(super) row: u64,
    pub(super) col: u64,
    pub(super) x: u64,
    pub(super) y: u64,
    pub(super) width: u32,
    pub(super) height: u32,
    pub(super) source_j2k_dimensions: Option<(u32, u32)>,
    pub(super) source_j2k_syntax: Option<CompressedTransferSyntax>,
    pub(super) source_j2k_profile: Option<PixelProfile>,
    pub(super) source_j2k: Option<j2k_direct_htj2k::Frame>,
    pub(super) source_jpeg: Option<jpeg_direct_htj2k::Frame>,
    pub(super) source_jpeg_retiled: bool,
    pub(super) source_jpeg_retile_duration: Duration,
    pub(super) source_jpeg_retile_rejection: Option<JpegRetileRejectionReason>,
    pub(super) source_jpeg_direct_rejected: bool,
    pub(super) source_raw_probe_failed: bool,
    pub(super) passthrough: Option<J2kPassthroughFrame>,
}

impl LosslessJ2kPlannedFrame {
    pub(super) fn rect(&self) -> OutputFrameRect {
        OutputFrameRect::new(self.x, self.y, self.width, self.height)
    }

    pub(crate) fn has_passthrough(&self) -> bool {
        self.passthrough.is_some()
    }

    pub(crate) fn has_j2k_source(&self) -> bool {
        self.source_j2k_syntax.is_some()
    }
}

#[derive(Clone)]
pub(super) struct J2kPassthroughFrame {
    pub(super) codestream: Vec<u8>,
    pub(super) profile: PixelProfile,
    pub(super) transfer_syntax: CompressedTransferSyntax,
}

impl J2kPassthroughFrame {
    pub(super) fn is_lossy(&self) -> bool {
        matches!(
            self.transfer_syntax,
            CompressedTransferSyntax::Jpeg2000Lossy | CompressedTransferSyntax::HtJpeg2000Lossy
        )
    }
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct LosslessJ2kPlanRequest {
    pub(crate) location: InstanceCoordinate,
    pub(crate) start_row: u64,
    pub(crate) row_count: u64,
    pub(crate) start_col: u64,
    pub(crate) tile_count: u64,
    pub(crate) grid: FrameRectGrid,
    pub(crate) transfer_syntax: TransferSyntax,
    pub(crate) allow_passthrough_probe: bool,
}

pub(crate) fn plan_lossless_j2k_frames(
    slide: &Slide,
    request: LosslessJ2kPlanRequest,
) -> Result<Vec<LosslessJ2kPlannedFrame>, Error> {
    let rows = usize::try_from(request.row_count).map_err(|_| Error::Unsupported {
        reason: "J2K row planning row count exceeds platform addressable memory".into(),
    })?;
    let tiles = usize::try_from(request.tile_count).map_err(|_| Error::Unsupported {
        reason: "J2K row planning tile count exceeds platform addressable memory".into(),
    })?;
    let mut planned = Vec::with_capacity(rows.saturating_mul(tiles));
    for offset in 0..request.row_count {
        let row = request
            .start_row
            .checked_add(offset)
            .ok_or_else(|| Error::Unsupported {
                reason: "J2K row planning tile row overflow".into(),
            })?;
        planned.extend(plan_lossless_j2k_row_at(slide, request, row)?);
    }
    Ok(planned)
}

fn plan_lossless_j2k_row_at(
    slide: &Slide,
    request: LosslessJ2kPlanRequest,
    row: u64,
) -> Result<Vec<LosslessJ2kPlannedFrame>, Error> {
    let tile_count = usize::try_from(request.tile_count).map_err(|_| Error::Unsupported {
        reason: "J2K row planning tile count exceeds platform addressable memory".into(),
    })?;
    let row_i64 = i64::try_from(row).map_err(|_| Error::Unsupported {
        reason: "J2K row planning tile row exceeds i64".into(),
    })?;
    let mut planned = Vec::with_capacity(tile_count);
    for offset in 0..tile_count {
        let col = request
            .start_col
            .checked_add(u64::try_from(offset).map_err(|_| Error::Unsupported {
                reason: "J2K row planning tile offset exceeds u64".into(),
            })?)
            .ok_or_else(|| Error::Unsupported {
                reason: "J2K row planning tile column overflow".into(),
            })?;
        let col_i64 = i64::try_from(col).map_err(|_| Error::Unsupported {
            reason: "J2K row planning tile column exceeds i64".into(),
        })?;
        let rect = OutputFrameRect::clamped(
            col,
            row,
            request.grid,
            FrameRectOverflowReasons {
                x: "J2K row planning tile x offset overflow",
                y: "J2K row planning tile y offset overflow",
            },
        )?;
        let allow_raw_probe = request.allow_passthrough_probe
            || jpeg_direct_htj2k::transfer_syntax(request.transfer_syntax);
        let (
            source_j2k_dimensions,
            source_j2k_syntax,
            source_j2k_profile,
            source_j2k,
            mut source_jpeg,
            source_jpeg_direct_rejected,
            source_raw_probe_failed,
            passthrough,
        ) = if allow_raw_probe {
            let tile_request = request.location.tile_request(col_i64, row_i64);
            match slide.read_raw_compressed_tile(&tile_request) {
                Ok(raw) => {
                    let source_j2k_dimensions = Some((raw.width(), raw.height()));
                    let (source_j2k_syntax, source_j2k_profile) =
                        j2k_raw_frame_syntax_and_profile(&raw);
                    let source_j2k = j2k_direct_htj2k::frame(
                        &raw,
                        request.grid.frame_columns,
                        request.grid.frame_rows,
                        request.transfer_syntax,
                        source_j2k_profile,
                    );
                    let source_jpeg = jpeg_direct_htj2k::frame(
                        &raw,
                        request.grid.frame_columns,
                        request.grid.frame_rows,
                        request.transfer_syntax,
                    );
                    let source_jpeg_direct_rejected =
                        jpeg_direct_htj2k::transfer_syntax(request.transfer_syntax)
                            && raw.compression() == Compression::Jpeg
                            && source_jpeg.is_none();
                    let passthrough = if request.allow_passthrough_probe {
                        j2k_passthrough_frame(
                            raw,
                            request.grid.frame_columns,
                            request.grid.frame_rows,
                            request.transfer_syntax,
                        )?
                    } else {
                        None
                    };
                    (
                        source_j2k_dimensions,
                        source_j2k_syntax,
                        source_j2k_profile,
                        source_j2k,
                        source_jpeg,
                        source_jpeg_direct_rejected,
                        false,
                        passthrough,
                    )
                }
                Err(_) => (None, None, None, None, None, false, true, None),
            }
        } else {
            (None, None, None, None, None, false, false, None)
        };
        let mut source_jpeg_retiled = false;
        let mut source_jpeg_retile_duration = Duration::ZERO;
        let mut source_jpeg_retile_rejection = None;
        if source_jpeg.is_none() && jpeg_direct_htj2k::transfer_syntax(request.transfer_syntax) {
            match read_raw_jpeg_retile_display_tile(
                slide,
                request.location,
                col,
                row,
                request.grid.frame_columns,
                request.grid.frame_rows,
            )? {
                RawJpegRetileProbe::Accepted(retiled) => {
                    source_jpeg = jpeg_direct_htj2k::frame(
                        &retiled.raw,
                        request.grid.frame_columns,
                        request.grid.frame_rows,
                        request.transfer_syntax,
                    );
                    if source_jpeg.is_some() {
                        source_jpeg_retiled = true;
                        source_jpeg_retile_duration = retiled.duration;
                    } else {
                        source_jpeg_retile_rejection =
                            Some(JpegRetileRejectionReason::ProfileUnsupported);
                    }
                }
                RawJpegRetileProbe::Rejected(reason) => {
                    source_jpeg_retile_rejection = Some(reason);
                }
            }
        }
        planned.push(LosslessJ2kPlannedFrame {
            row,
            col,
            x: rect.x,
            y: rect.y,
            width: rect.width,
            height: rect.height,
            source_j2k_dimensions,
            source_j2k_syntax,
            source_j2k_profile,
            source_j2k,
            source_jpeg,
            source_jpeg_retiled,
            source_jpeg_retile_duration,
            source_jpeg_retile_rejection,
            source_jpeg_direct_rejected,
            source_raw_probe_failed,
            passthrough,
        });
    }
    Ok(planned)
}