1use std::error::Error as StdError;
2use std::fmt;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use serde::Serialize;
7
8use super::MetadataInput;
9use crate::{
10 export_dicom, validate_dicom_path, AnnotationCoordinateSpace, ColorManagement, Error,
11 ExportOptions, ExportReport, ExportRequest, QuPathAnnotationOptions, ValidationOptions,
12 ValidationReport,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "snake_case")]
18#[non_exhaustive]
19pub enum ExportWorkflowStage {
20 Metadata,
22 Annotations,
24 Export,
26 Validation,
28 Report,
30}
31
32impl fmt::Display for ExportWorkflowStage {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 let label = match self {
35 Self::Metadata => "metadata",
36 Self::Annotations => "annotations",
37 Self::Export => "export",
38 Self::Validation => "validation",
39 Self::Report => "report",
40 };
41 formatter.write_str(label)
42 }
43}
44
45#[derive(Debug)]
47pub struct ExportWorkflowError {
48 stage: ExportWorkflowStage,
49 source: Error,
50}
51
52impl ExportWorkflowError {
53 pub(crate) const fn new(stage: ExportWorkflowStage, source: Error) -> Self {
54 Self { stage, source }
55 }
56
57 #[must_use]
59 pub const fn stage(&self) -> ExportWorkflowStage {
60 self.stage
61 }
62
63 #[must_use]
65 pub const fn source_error(&self) -> &Error {
66 &self.source
67 }
68}
69
70impl fmt::Display for ExportWorkflowError {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(formatter, "{} stage failed: {}", self.stage, self.source)
73 }
74}
75
76impl StdError for ExportWorkflowError {
77 fn source(&self) -> Option<&(dyn StdError + 'static)> {
78 Some(&self.source)
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[non_exhaustive]
85pub enum ExportWorkflowProgressEvent {
86 ResolvingMetadata,
88 PreparingAnnotations,
90 Exporting,
92 Validating,
94 PersistingReport,
96 Complete,
98}
99
100pub trait ExportWorkflowProgress: Send + Sync {
102 fn on_progress(&self, event: ExportWorkflowProgressEvent);
104}
105
106pub struct ExportWorkflowRequest {
108 pub source_path: PathBuf,
110 pub output_dir: PathBuf,
112 pub options: ExportOptions,
114 pub color_management: ColorManagement,
116 pub metadata: MetadataInput,
118 pub level_filter: Option<u32>,
120 pub annotations: Option<QuPathAnnotationOptions>,
122 pub validation: Option<ValidationOptions>,
124 pub report_path: Option<PathBuf>,
126 pub progress: Option<Arc<dyn ExportWorkflowProgress>>,
128}
129
130impl ExportWorkflowRequest {
131 #[must_use]
133 pub fn new(
134 source_path: impl Into<PathBuf>,
135 output_dir: impl Into<PathBuf>,
136 options: ExportOptions,
137 color_management: ColorManagement,
138 metadata: MetadataInput,
139 ) -> Self {
140 Self {
141 source_path: source_path.into(),
142 output_dir: output_dir.into(),
143 options,
144 color_management,
145 metadata,
146 level_filter: None,
147 annotations: None,
148 validation: None,
149 report_path: None,
150 progress: None,
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
157#[non_exhaustive]
158pub struct ExportWorkflowReport {
159 pub export: ExportReport,
161 pub validation: Option<ValidationReport>,
163}
164
165impl ExportWorkflowReport {
166 pub fn to_pretty_json(&self) -> Result<String, Error> {
168 serde_json::to_string_pretty(self).map_err(|error| Error::JsonSerialize {
169 message: error.to_string(),
170 })
171 }
172
173 #[must_use]
175 pub fn summary(&self) -> String {
176 let annotation_instances = self
177 .export
178 .annotations
179 .as_ref()
180 .map_or(0, |annotations| annotations.instances.len());
181 match &self.validation {
182 Some(validation) => format!(
183 "Exported {} WSI instance(s) and {} annotation sidecar(s); validation passed={} failed={} skipped={}.",
184 self.export.instances.len(),
185 annotation_instances,
186 validation.passed_checks(),
187 validation.failed_checks(),
188 validation.skipped_checks(),
189 ),
190 None => format!(
191 "Exported {} WSI instance(s) and {} annotation sidecar(s).",
192 self.export.instances.len(),
193 annotation_instances,
194 ),
195 }
196 }
197}
198
199pub fn run_export_workflow(
201 request: ExportWorkflowRequest,
202) -> Result<ExportWorkflowReport, ExportWorkflowError> {
203 emit(
204 &request.progress,
205 ExportWorkflowProgressEvent::ResolvingMetadata,
206 );
207 let metadata = request.metadata.resolve()?;
208
209 emit(
210 &request.progress,
211 ExportWorkflowProgressEvent::PreparingAnnotations,
212 );
213 let prepared_annotations = match request.annotations {
214 Some(annotations) => {
215 if annotations.coordinate_space == AnnotationCoordinateSpace::Level0Pixels
216 && request.level_filter.is_some_and(|level| level != 0)
217 {
218 return Err(ExportWorkflowError::new(
219 ExportWorkflowStage::Annotations,
220 Error::InvalidOptions {
221 reason: "level-zero QuPath coordinates require exporting level 0".into(),
222 },
223 ));
224 }
225 Some(annotations.prepare().map_err(|error| {
226 ExportWorkflowError::new(ExportWorkflowStage::Annotations, error)
227 })?)
228 }
229 None => None,
230 };
231
232 emit(&request.progress, ExportWorkflowProgressEvent::Exporting);
233 let mut export = export_dicom(ExportRequest {
234 source_path: request.source_path,
235 output_dir: request.output_dir.clone(),
236 options: request.options,
237 color_management: request.color_management,
238 metadata,
239 level_filter: request.level_filter,
240 })
241 .map_err(|error| ExportWorkflowError::new(ExportWorkflowStage::Export, error))?;
242
243 if let Some(annotations) = prepared_annotations {
244 export.annotations =
245 Some(annotations.export_for(&export).map_err(|error| {
246 ExportWorkflowError::new(ExportWorkflowStage::Annotations, error)
247 })?);
248 }
249
250 let validation = match request.validation {
251 Some(options) => {
252 emit(&request.progress, ExportWorkflowProgressEvent::Validating);
253 Some(
254 validate_dicom_path(&request.output_dir, &options).map_err(|error| {
255 ExportWorkflowError::new(ExportWorkflowStage::Validation, error)
256 })?,
257 )
258 }
259 None => None,
260 };
261
262 let report = ExportWorkflowReport { export, validation };
263 if let Some(path) = request.report_path {
264 emit(
265 &request.progress,
266 ExportWorkflowProgressEvent::PersistingReport,
267 );
268 let json = report
269 .to_pretty_json()
270 .map_err(|error| ExportWorkflowError::new(ExportWorkflowStage::Report, error))?;
271 std::fs::write(&path, json).map_err(|source| {
272 ExportWorkflowError::new(ExportWorkflowStage::Report, Error::Io { path, source })
273 })?;
274 }
275 emit(&request.progress, ExportWorkflowProgressEvent::Complete);
276 Ok(report)
277}
278
279fn emit(progress: &Option<Arc<dyn ExportWorkflowProgress>>, event: ExportWorkflowProgressEvent) {
280 if let Some(progress) = progress {
281 progress.on_progress(event);
282 }
283}