1use std::path::PathBuf;
4use std::time::Duration;
5
6use crate::{
7 CodecValidation, EncodeBackendPreference, Error, ExportOptions, MetadataSource, TransferSyntax,
8};
9
10#[derive(Debug, Clone, PartialEq)]
12#[non_exhaustive]
13pub struct ExportRequest {
14 pub source_path: PathBuf,
16 pub output_dir: PathBuf,
18 pub options: ExportOptions,
20 pub metadata: MetadataSource,
22 pub level_filter: Option<u32>,
24}
25
26impl ExportRequest {
27 pub fn new(
29 source_path: PathBuf,
30 output_dir: PathBuf,
31 options: ExportOptions,
32 metadata: MetadataSource,
33 ) -> Result<Self, Error> {
34 options.validate()?;
35 Ok(Self {
36 source_path,
37 output_dir,
38 options,
39 metadata,
40 level_filter: None,
41 })
42 }
43
44 pub fn validate(&self) -> Result<(), Error> {
46 self.options.validate()
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52#[non_exhaustive]
53pub struct FrameSamples<'a> {
54 pub data: &'a [u8],
56 pub width: u32,
58 pub height: u32,
60 pub components: u8,
62 pub bit_depth: u8,
64 pub signed: bool,
66}
67
68impl<'a> FrameSamples<'a> {
69 pub fn new(
71 data: &'a [u8],
72 width: u32,
73 height: u32,
74 components: u8,
75 bit_depth: u8,
76 signed: bool,
77 ) -> Result<Self, Error> {
78 let samples = Self {
79 data,
80 width,
81 height,
82 components,
83 bit_depth,
84 signed,
85 };
86 samples.to_j2k()?;
87 Ok(samples)
88 }
89
90 pub(crate) fn to_j2k(self) -> Result<j2k::J2kLosslessSamples<'a>, Error> {
91 j2k::J2kLosslessSamples::new(
92 self.data,
93 self.width,
94 self.height,
95 u16::from(self.components),
96 self.bit_depth,
97 self.signed,
98 )
99 .map_err(|err| Error::UnsupportedPixelData {
100 reason: err.to_string(),
101 })
102 }
103}
104
105#[derive(Debug, Clone, Copy)]
107#[non_exhaustive]
108pub struct J2kFrameEncodeRequest<'a> {
109 pub samples: FrameSamples<'a>,
111 pub transfer_syntax: TransferSyntax,
113 pub encode_backend: EncodeBackendPreference,
115 pub codec_validation: CodecValidation,
117}
118
119impl<'a> J2kFrameEncodeRequest<'a> {
120 pub fn new(
122 samples: FrameSamples<'a>,
123 transfer_syntax: TransferSyntax,
124 encode_backend: EncodeBackendPreference,
125 codec_validation: CodecValidation,
126 ) -> Self {
127 Self {
128 samples,
129 transfer_syntax,
130 encode_backend,
131 codec_validation,
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq)]
138#[non_exhaustive]
139pub struct RouteProfileRequest {
140 pub source_path: PathBuf,
142 pub options: ExportOptions,
144 pub source_aware_transfer_syntax: bool,
146 pub level: u32,
148 pub max_frames: u64,
150}
151
152impl RouteProfileRequest {
153 pub fn new(source_path: PathBuf, options: ExportOptions, level: u32, max_frames: u64) -> Self {
155 Self {
156 source_path,
157 options,
158 source_aware_transfer_syntax: true,
159 level,
160 max_frames,
161 }
162 }
163
164 pub fn with_source_aware_transfer_syntax(mut self, source_aware: bool) -> Self {
166 self.source_aware_transfer_syntax = source_aware;
167 self
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173#[non_exhaustive]
174pub struct DefaultTransferSyntaxRequest {
175 pub source_path: PathBuf,
177 pub tile_size: u32,
179 pub level_filter: Option<u32>,
181 pub max_levels: Option<u32>,
183}
184
185impl DefaultTransferSyntaxRequest {
186 pub fn new(source_path: PathBuf, tile_size: u32) -> Self {
188 Self {
189 source_path,
190 tile_size,
191 level_filter: None,
192 max_levels: None,
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
199#[non_exhaustive]
200pub enum RouteCoverageTarget {
201 Source(PathBuf),
203 Corpus(PathBuf),
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209#[non_exhaustive]
210pub enum RouteProgressSink {
211 Stderr,
213}
214
215#[derive(Debug, Clone, PartialEq)]
217#[non_exhaustive]
218pub struct RouteCoverageRequest {
219 pub target: RouteCoverageTarget,
221 pub options: ExportOptions,
223 pub source_aware_transfer_syntax: bool,
225 pub max_frames_per_level: u64,
227 pub max_levels: Option<u32>,
229 pub max_level_elapsed: Option<Duration>,
231 pub progress: Option<RouteProgressSink>,
233 pub max_sources: usize,
235 pub max_depth: usize,
237}
238
239impl RouteCoverageRequest {
240 pub fn new(source_path: PathBuf, options: ExportOptions) -> Self {
242 Self {
243 target: RouteCoverageTarget::Source(source_path),
244 options,
245 source_aware_transfer_syntax: true,
246 max_frames_per_level: 1,
247 max_levels: None,
248 max_level_elapsed: None,
249 progress: None,
250 max_sources: 100_000,
251 max_depth: 64,
252 }
253 }
254
255 pub fn new_corpus(source_root: PathBuf, options: ExportOptions) -> Self {
257 Self {
258 target: RouteCoverageTarget::Corpus(source_root),
259 options,
260 source_aware_transfer_syntax: true,
261 max_frames_per_level: 1,
262 max_levels: None,
263 max_level_elapsed: None,
264 progress: None,
265 max_sources: 100_000,
266 max_depth: 64,
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn request_constructors_preserve_defaults_and_validate_options() {
277 let source = PathBuf::from("slide.svs");
278 let output = PathBuf::from("dicom-out");
279 let options = ExportOptions::default();
280
281 let export = ExportRequest::new(
282 source.clone(),
283 output.clone(),
284 options.clone(),
285 MetadataSource::ResearchPlaceholder,
286 )
287 .unwrap();
288 assert_eq!(export.source_path, source);
289 assert_eq!(export.output_dir, output);
290 assert_eq!(export.metadata, MetadataSource::ResearchPlaceholder);
291 assert_eq!(export.level_filter, None);
292 export.validate().unwrap();
293
294 let err = ExportRequest::new(
295 PathBuf::from("slide.svs"),
296 PathBuf::from("dicom-out"),
297 ExportOptions {
298 tile_size: 0,
299 ..ExportOptions::default()
300 },
301 MetadataSource::ResearchPlaceholder,
302 )
303 .expect_err("zero tile size should be rejected");
304 assert!(err.to_string().contains("tile_size"));
305
306 let profile = RouteProfileRequest::new(PathBuf::from("slide.svs"), options.clone(), 2, 16);
307 assert!(profile.source_aware_transfer_syntax);
308 assert_eq!(profile.level, 2);
309 assert_eq!(profile.max_frames, 16);
310
311 let default_transfer = DefaultTransferSyntaxRequest::new(PathBuf::from("slide.svs"), 512);
312 assert_eq!(default_transfer.tile_size, 512);
313 assert_eq!(default_transfer.level_filter, None);
314 assert_eq!(default_transfer.max_levels, None);
315
316 let coverage = RouteCoverageRequest::new(PathBuf::from("slide.svs"), options.clone());
317 assert_eq!(
318 coverage.target,
319 RouteCoverageTarget::Source(PathBuf::from("slide.svs"))
320 );
321 assert!(coverage.source_aware_transfer_syntax);
322 assert_eq!(coverage.max_frames_per_level, 1);
323 assert_eq!(coverage.max_levels, None);
324 assert_eq!(coverage.max_level_elapsed, None);
325 assert_eq!(coverage.progress, None);
326 assert_eq!(coverage.max_sources, 100_000);
327 assert_eq!(coverage.max_depth, 64);
328
329 let corpus = RouteCoverageRequest::new_corpus(PathBuf::from("slides"), options);
330 assert_eq!(
331 corpus.target,
332 RouteCoverageTarget::Corpus(PathBuf::from("slides"))
333 );
334 assert!(corpus.source_aware_transfer_syntax);
335 assert_eq!(corpus.max_frames_per_level, 1);
336 assert_eq!(corpus.max_levels, None);
337 assert_eq!(corpus.max_level_elapsed, None);
338 assert_eq!(corpus.progress, None);
339 assert_eq!(corpus.max_sources, 100_000);
340 assert_eq!(corpus.max_depth, 64);
341 }
342
343 #[test]
344 fn frame_encode_request_constructors_validate_sample_geometry() {
345 let pixels = [0_u8; 2 * 2 * 3];
346 let samples = FrameSamples::new(&pixels, 2, 2, 3, 8, false).unwrap();
347 assert_eq!(samples.width, 2);
348 assert_eq!(samples.height, 2);
349 assert_eq!(samples.components, 3);
350
351 let request = J2kFrameEncodeRequest::new(
352 samples,
353 TransferSyntax::Htj2kLosslessRpcl,
354 EncodeBackendPreference::CpuOnly,
355 CodecValidation::RoundTrip,
356 );
357 assert_eq!(request.transfer_syntax, TransferSyntax::Htj2kLosslessRpcl);
358 assert_eq!(request.encode_backend, EncodeBackendPreference::CpuOnly);
359 assert_eq!(request.codec_validation, CodecValidation::RoundTrip);
360
361 let err = FrameSamples::new(&pixels[..3], 2, 2, 3, 8, false)
362 .expect_err("short frame data should be rejected");
363 assert!(err.to_string().contains("pixel"));
364 }
365}