1use std::path::PathBuf;
4
5use crate::{
6 default_transfer_syntax_for_source, export_dicom, CodecValidation,
7 DefaultTransferSyntaxRequest, EncodeBackendPreference, Error, ExportOptions, ExportReport,
8 ExportRequest, IccProfilePolicy, JpegDirectHtj2kProfile, MetadataSource, TransferSyntax,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum TransferSyntaxSelection {
13 SourceAware,
14 Explicit,
15}
16
17#[derive(Debug, Clone)]
19#[must_use = "Export is a builder; call run() to execute the export"]
20pub struct Export {
21 source_path: PathBuf,
22 output_dir: Option<PathBuf>,
23 options: ExportOptions,
24 metadata: Option<MetadataSource>,
25 level_filter: Option<u32>,
26 transfer_syntax: TransferSyntaxSelection,
27}
28
29impl Export {
30 #[must_use = "call run() or build_request() on the returned Export builder"]
37 pub fn from_slide(source_path: impl Into<PathBuf>) -> Self {
38 Self {
39 source_path: source_path.into(),
40 output_dir: None,
41 options: ExportOptions::default(),
42 metadata: None,
43 level_filter: None,
44 transfer_syntax: TransferSyntaxSelection::SourceAware,
45 }
46 }
47
48 #[must_use = "builder methods return the updated Export"]
50 pub fn to_directory(mut self, output_dir: impl Into<PathBuf>) -> Self {
51 self.output_dir = Some(output_dir.into());
52 self
53 }
54
55 #[must_use = "builder methods return the updated Export"]
57 pub fn with_options(mut self, options: ExportOptions) -> Self {
58 self.options = options;
59 self.transfer_syntax = TransferSyntaxSelection::Explicit;
60 self
61 }
62
63 #[must_use = "builder methods return the updated Export"]
65 pub fn with_metadata(mut self, metadata: MetadataSource) -> Self {
66 self.metadata = Some(metadata);
67 self
68 }
69
70 #[must_use = "builder methods return the updated Export"]
72 pub fn with_research_placeholder_metadata(mut self) -> Self {
73 self.metadata = Some(MetadataSource::ResearchPlaceholder);
74 self
75 }
76
77 #[must_use = "builder methods return the updated Export"]
79 pub fn level(mut self, level: u32) -> Self {
80 self.level_filter = Some(level);
81 self
82 }
83
84 #[must_use = "builder methods return the updated Export"]
86 pub fn transfer_syntax(mut self, transfer_syntax: TransferSyntax) -> Self {
87 self.options.transfer_syntax = transfer_syntax;
88 self.options.jpeg_direct_htj2k_profile =
89 JpegDirectHtj2kProfile::default_for_transfer_syntax(transfer_syntax);
90 self.transfer_syntax = TransferSyntaxSelection::Explicit;
91 self
92 }
93
94 #[must_use = "builder methods return the updated Export"]
96 pub fn jpeg_direct_htj2k_profile(mut self, profile: JpegDirectHtj2kProfile) -> Self {
97 self.options.jpeg_direct_htj2k_profile = profile;
98 self
99 }
100
101 #[must_use = "builder methods return the updated Export"]
103 pub fn tile_size(mut self, tile_size: u32) -> Self {
104 self.options.tile_size = tile_size;
105 self
106 }
107
108 #[must_use = "builder methods return the updated Export"]
110 pub fn jpeg_quality(mut self, jpeg_quality: u8) -> Self {
111 self.options.jpeg_quality = jpeg_quality;
112 self
113 }
114
115 #[must_use = "builder methods return the updated Export"]
117 pub fn icc_profile_policy(mut self, policy: IccProfilePolicy) -> Self {
118 self.options.icc_profile_policy = policy;
119 self
120 }
121
122 #[must_use = "builder methods return the updated Export"]
124 pub fn encode_backend(mut self, backend: EncodeBackendPreference) -> Self {
125 self.options.encode_backend = backend;
126 self
127 }
128
129 #[must_use = "builder methods return the updated Export"]
131 pub fn codec_validation(mut self, validation: CodecValidation) -> Self {
132 self.options.codec_validation = validation;
133 self
134 }
135
136 #[must_use = "builder methods return the updated Export"]
138 pub fn source_device_decode(mut self, source_device_decode: bool) -> Self {
139 self.options.source_device_decode = source_device_decode;
140 self
141 }
142
143 #[must_use = "builder methods return the updated Export"]
145 pub fn j2k_decomposition_levels(mut self, levels: Option<u8>) -> Self {
146 self.options.j2k_decomposition_levels = levels;
147 self
148 }
149
150 #[must_use = "builder methods return the updated Export"]
152 pub fn gpu_encode_inflight_tiles(mut self, tiles: Option<usize>) -> Self {
153 self.options.gpu_encode_inflight_tiles = tiles;
154 self
155 }
156
157 #[must_use = "builder methods return the updated Export"]
159 pub fn gpu_encode_memory_mib(mut self, memory_mib: Option<u64>) -> Self {
160 self.options.gpu_encode_memory_mib = memory_mib;
161 self
162 }
163
164 #[must_use = "builder methods return the updated Export"]
166 pub fn gpu_pipeline_depth(mut self, depth: Option<usize>) -> Self {
167 self.options.gpu_pipeline_depth = depth;
168 self
169 }
170
171 #[must_use = "builder methods return the updated Export"]
173 pub fn gpu_row_batch_rows(mut self, rows: Option<usize>) -> Self {
174 self.options.gpu_row_batch_rows = rows;
175 self
176 }
177
178 #[must_use = "builder methods return the updated Export"]
180 pub fn gpu_row_batch_target_tiles(mut self, tiles: Option<usize>) -> Self {
181 self.options.gpu_row_batch_target_tiles = tiles;
182 self
183 }
184
185 #[must_use = "builder methods return the updated Export"]
187 pub fn source_aware_transfer_syntax(mut self) -> Self {
188 self.transfer_syntax = TransferSyntaxSelection::SourceAware;
189 self
190 }
191
192 pub fn build_request(mut self) -> Result<ExportRequest, Error> {
194 let output_dir = self
195 .output_dir
196 .take()
197 .ok_or_else(|| Error::InvalidOptions {
198 reason: "output directory must be configured with to_directory".into(),
199 })?;
200 if self.transfer_syntax == TransferSyntaxSelection::SourceAware {
201 self.options.transfer_syntax =
202 default_transfer_syntax_for_source(DefaultTransferSyntaxRequest {
203 source_path: self.source_path.clone(),
204 tile_size: self.options.tile_size,
205 level_filter: self.level_filter,
206 max_levels: None,
207 })?;
208 self.options.jpeg_direct_htj2k_profile =
209 JpegDirectHtj2kProfile::default_for_transfer_syntax(self.options.transfer_syntax);
210 }
211 let metadata = self.metadata.take().ok_or_else(|| Error::Metadata {
212 reason: "export metadata must be provided with with_metadata or with_research_placeholder_metadata".into(),
213 })?;
214 self.options.validate()?;
215 Ok(ExportRequest {
216 source_path: self.source_path,
217 output_dir,
218 options: self.options,
219 metadata,
220 level_filter: self.level_filter,
221 })
222 }
223
224 pub fn run(self) -> Result<ExportReport, Error> {
226 export_dicom(self.build_request()?)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use crate::{
233 CodecValidation, EncodeBackendPreference, Export, IccProfilePolicy, JpegDirectHtj2kProfile,
234 MetadataSource, TransferSyntax,
235 };
236
237 #[test]
238 fn htj2k_transfer_syntax_defaults_to_97_profile() {
239 let request = Export::from_slide("source.ndpi")
240 .to_directory("dicom-out")
241 .with_research_placeholder_metadata()
242 .transfer_syntax(TransferSyntax::Htj2k)
243 .build_request()
244 .unwrap();
245
246 assert_eq!(request.options.transfer_syntax, TransferSyntax::Htj2k);
247 assert_eq!(
248 request.options.jpeg_direct_htj2k_profile,
249 JpegDirectHtj2kProfile::Lossy97
250 );
251 }
252
253 #[test]
254 fn builder_option_setters_flow_into_request() {
255 let request = Export::from_slide("source.ndpi")
256 .to_directory("dicom-out")
257 .with_metadata(MetadataSource::ResearchPlaceholder)
258 .transfer_syntax(TransferSyntax::Htj2kLossless)
259 .tile_size(256)
260 .jpeg_quality(80)
261 .icc_profile_policy(IccProfilePolicy::OmitIfMissing)
262 .encode_backend(EncodeBackendPreference::CpuOnly)
263 .codec_validation(CodecValidation::RoundTrip)
264 .source_device_decode(true)
265 .j2k_decomposition_levels(Some(3))
266 .gpu_encode_inflight_tiles(Some(8))
267 .gpu_encode_memory_mib(Some(4096))
268 .gpu_pipeline_depth(Some(3))
269 .gpu_row_batch_rows(Some(6))
270 .gpu_row_batch_target_tiles(Some(96))
271 .build_request()
272 .unwrap();
273
274 assert_eq!(request.options.tile_size, 256);
275 assert_eq!(request.options.jpeg_quality, 80);
276 assert_eq!(
277 request.options.icc_profile_policy,
278 IccProfilePolicy::OmitIfMissing
279 );
280 assert_eq!(
281 request.options.encode_backend,
282 EncodeBackendPreference::CpuOnly
283 );
284 assert_eq!(request.options.codec_validation, CodecValidation::RoundTrip);
285 assert!(request.options.source_device_decode);
286 assert_eq!(request.options.j2k_decomposition_levels, Some(3));
287 assert_eq!(request.options.gpu_encode_inflight_tiles, Some(8));
288 assert_eq!(request.options.gpu_encode_memory_mib, Some(4096));
289 assert_eq!(request.options.gpu_pipeline_depth, Some(3));
290 assert_eq!(request.options.gpu_row_batch_rows, Some(6));
291 assert_eq!(request.options.gpu_row_batch_target_tiles, Some(96));
292 }
293
294 #[test]
295 fn builder_requires_explicit_metadata() {
296 let err = Export::from_slide("source.ndpi")
297 .to_directory("dicom-out")
298 .transfer_syntax(TransferSyntax::Htj2kLossless)
299 .build_request()
300 .expect_err("metadata policy must be explicit");
301
302 assert!(err.to_string().contains("metadata"));
303 }
304}