Skip to main content

wsi_dicom/
options.rs

1use serde::{Deserialize, Serialize};
2
3use crate::WsiDicomError;
4
5/// Runtime preference for JPEG 2000 Lossless encode backends.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum EncodeBackendPreference {
8    Auto,
9    CpuOnly,
10    PreferDevice,
11    RequireDevice,
12}
13
14impl EncodeBackendPreference {
15    pub(crate) fn to_signinum(self) -> signinum_j2k::EncodeBackendPreference {
16        match self {
17            Self::Auto => signinum_j2k::EncodeBackendPreference::Auto,
18            Self::CpuOnly => signinum_j2k::EncodeBackendPreference::CpuOnly,
19            Self::PreferDevice => signinum_j2k::EncodeBackendPreference::PreferDevice,
20            Self::RequireDevice => signinum_j2k::EncodeBackendPreference::RequireDevice,
21        }
22    }
23}
24
25/// Runtime validation policy for newly encoded compressed frame bytes.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27pub enum CodecValidation {
28    Disabled,
29    RoundTrip,
30}
31
32impl CodecValidation {
33    pub(crate) fn to_j2k_validation(self) -> signinum_j2k::J2kEncodeValidation {
34        match self {
35            Self::Disabled => signinum_j2k::J2kEncodeValidation::External,
36            Self::RoundTrip => signinum_j2k::J2kEncodeValidation::CpuRoundTrip,
37        }
38    }
39
40    #[allow(dead_code)]
41    pub(crate) fn enabled(self) -> bool {
42        self == Self::RoundTrip
43    }
44}
45
46/// DICOM transfer syntax choices for exported VL Whole Slide Microscopy files.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum TransferSyntax {
49    JpegBaseline8Bit,
50    Jpeg2000,
51    Jpeg2000Lossless,
52    Htj2kLossless,
53    Htj2kLosslessRpcl,
54    ExplicitVrLittleEndian,
55}
56
57impl TransferSyntax {
58    pub fn uid(self) -> &'static str {
59        match self {
60            Self::JpegBaseline8Bit => "1.2.840.10008.1.2.4.50",
61            Self::Jpeg2000 => "1.2.840.10008.1.2.4.91",
62            Self::Jpeg2000Lossless => "1.2.840.10008.1.2.4.90",
63            Self::Htj2kLossless => "1.2.840.10008.1.2.4.201",
64            Self::Htj2kLosslessRpcl => "1.2.840.10008.1.2.4.202",
65            Self::ExplicitVrLittleEndian => "1.2.840.10008.1.2.1",
66        }
67    }
68
69    pub(crate) fn is_j2k_family(self) -> bool {
70        matches!(
71            self,
72            Self::Jpeg2000 | Self::Jpeg2000Lossless | Self::Htj2kLossless | Self::Htj2kLosslessRpcl
73        )
74    }
75
76    pub(crate) fn is_lossless_j2k_family(self) -> bool {
77        matches!(
78            self,
79            Self::Jpeg2000Lossless | Self::Htj2kLossless | Self::Htj2kLosslessRpcl
80        )
81    }
82
83    pub(crate) fn is_jpeg2000_passthrough_only(self) -> bool {
84        self == Self::Jpeg2000
85    }
86}
87
88/// Options controlling how a source WSI should be converted into DICOM.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct DicomExportOptions {
91    pub tile_size: u32,
92    pub transfer_syntax: TransferSyntax,
93    pub jpeg_quality: u8,
94    pub encode_backend: EncodeBackendPreference,
95    pub codec_validation: CodecValidation,
96    pub source_device_decode: bool,
97    pub j2k_decomposition_levels: Option<u8>,
98    pub gpu_encode_inflight_tiles: Option<usize>,
99    pub gpu_encode_memory_mib: Option<u64>,
100}
101
102impl Default for DicomExportOptions {
103    fn default() -> Self {
104        Self {
105            tile_size: 512,
106            transfer_syntax: TransferSyntax::Htj2kLosslessRpcl,
107            jpeg_quality: 90,
108            encode_backend: EncodeBackendPreference::Auto,
109            codec_validation: CodecValidation::Disabled,
110            source_device_decode: false,
111            j2k_decomposition_levels: None,
112            gpu_encode_inflight_tiles: None,
113            gpu_encode_memory_mib: None,
114        }
115    }
116}
117
118impl DicomExportOptions {
119    pub fn validate(&self) -> Result<(), WsiDicomError> {
120        if self.tile_size == 0 {
121            return Err(WsiDicomError::InvalidOptions {
122                reason: "tile_size must be greater than zero".into(),
123            });
124        }
125        if !(1..=100).contains(&self.jpeg_quality) {
126            return Err(WsiDicomError::InvalidOptions {
127                reason: "jpeg_quality must be in the range 1..=100".into(),
128            });
129        }
130        if self.gpu_encode_inflight_tiles == Some(0) {
131            return Err(WsiDicomError::InvalidOptions {
132                reason: "gpu_encode_inflight_tiles must be greater than zero when provided".into(),
133            });
134        }
135        if self.gpu_encode_memory_mib == Some(0) {
136            return Err(WsiDicomError::InvalidOptions {
137                reason: "gpu_encode_memory_mib must be greater than zero when provided".into(),
138            });
139        }
140        if let Some(memory_mib) = self.gpu_encode_memory_mib {
141            let _ = usize::try_from(memory_mib)
142                .ok()
143                .and_then(|mib| mib.checked_mul(1024 * 1024))
144                .ok_or_else(|| WsiDicomError::InvalidOptions {
145                    reason: "gpu_encode_memory_mib exceeds platform addressable memory".into(),
146                })?;
147        }
148        Ok(())
149    }
150}