diff --git a/src/api/wado/routes.rs b/src/api/wado/routes.rs index a2cd627..938a3cb 100644 --- a/src/api/wado/routes.rs +++ b/src/api/wado/routes.rs @@ -70,6 +70,7 @@ async fn instance_resource( request: RetrieveInstanceRequest, ) -> impl IntoResponse { if let Some(wado) = provider.wado { + let transfer_syntax = request.transfer_syntax.clone(); let study_instance_uid: UI = request.query.study_instance_uid.clone(); let response = wado.retrieve(request).await; @@ -92,6 +93,7 @@ async fn instance_resource( ) .body(Body::from_stream(DicomMultipartStream::new( stream.into_stream(), + transfer_syntax.as_deref(), ))) .unwrap() } @@ -131,6 +133,7 @@ async fn rendered_resource( trace!("Using default rendering"); let instance_request = RetrieveInstanceRequest { query: request.query, + transfer_syntax: None, }; let stream = wado diff --git a/src/api/wado/service.rs b/src/api/wado/service.rs index aee00f1..11b8054 100644 --- a/src/api/wado/service.rs +++ b/src/api/wado/service.rs @@ -52,10 +52,10 @@ pub enum RetrieveError { impl IntoResponse for RetrieveError { fn into_response(self) -> Response { match self { - RetrieveError::Backend { source } => { + Self::Backend { source } => { (StatusCode::INTERNAL_SERVER_ERROR, source.to_string()).into_response() } - RetrieveError::Unimplemented => Response::builder() + Self::Unimplemented => Response::builder() .status(StatusCode::NOT_IMPLEMENTED) .body(Body::from("This transaction is not implemented.")) .unwrap(), @@ -64,6 +64,7 @@ impl IntoResponse for RetrieveError { } pub struct RetrieveInstanceRequest { pub query: ResourceQuery, + pub transfer_syntax: Option, } pub struct ThumbnailRequest { @@ -157,6 +158,33 @@ where } } +/// Extracts the transfer-syntax parameter from an Accept header value. +/// +/// According to +/// the syntax is: transfer-syntax-mtp = OWS ";" OWS %s"transfer-syntax=" ts-value +/// +/// Examples: +/// - "application/dicom; transfer-syntax=1.2.840.10008.1.2.4.50" +/// - "multipart/related; type=\"application/dicom\"; transfer-syntax=1.2.840.10008.1.2.4.50" +fn extract_transfer_syntax_from_accept(accept_header: &str) -> Option { + // Split by semicolons to get individual parameters + for part in accept_header.split(';') { + let trimmed = part.trim(); + // Look for the transfer-syntax parameter + if let Some(value) = trimmed.strip_prefix("transfer-syntax=") { + let value = value.trim(); + // The value might be quoted or unquoted + let transfer_syntax = if value.starts_with('"') && value.ends_with('"') { + value.trim_matches('"') + } else { + value + }; + return Some(transfer_syntax.to_string()); + } + } + None +} + impl FromRequestParts for RetrieveInstanceRequest where AppState: FromRef, @@ -169,7 +197,21 @@ where .await .map_err(PathRejection::into_response)?; - Ok(Self { query }) + let accept = parts + .headers + .get(ACCEPT) + .map(|h| String::from(h.to_str().unwrap_or_default())); + + // Extract the requested transfer-syntax from the Accept header if present + // https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.3.5.2.html + let transfer_syntax = accept + .as_ref() + .and_then(|accept_str| extract_transfer_syntax_from_accept(accept_str)); + + Ok(Self { + query, + transfer_syntax, + }) } } @@ -456,6 +498,59 @@ mod tests { ); } + #[test] + fn test_extract_transfer_syntax_from_accept() { + // Test simple case + assert_eq!( + extract_transfer_syntax_from_accept( + "application/dicom; transfer-syntax=1.2.840.10008.1.2.4.50" + ), + Some("1.2.840.10008.1.2.4.50".to_string()) + ); + + // Test with extra whitespace + assert_eq!( + extract_transfer_syntax_from_accept( + "application/dicom; transfer-syntax=1.2.840.10008.1.2.4.50" + ), + Some("1.2.840.10008.1.2.4.50".to_string()) + ); + + // Test wildcard + assert_eq!( + extract_transfer_syntax_from_accept("application/dicom; transfer-syntax=*"), + Some("*".to_string()) + ); + + // Test multipart/related with type parameter + assert_eq!( + extract_transfer_syntax_from_accept("multipart/related; type=\"application/dicom\"; transfer-syntax=1.2.840.10008.1.2.4.50"), + Some("1.2.840.10008.1.2.4.50".to_string()) + ); + + // Test with quoted value (though not typical for transfer-syntax) + assert_eq!( + extract_transfer_syntax_from_accept( + "application/dicom; transfer-syntax=\"1.2.840.10008.1.2.4.50\"" + ), + Some("1.2.840.10008.1.2.4.50".to_string()) + ); + + // Test without transfer-syntax parameter + assert_eq!( + extract_transfer_syntax_from_accept("application/dicom"), + None + ); + + // Test with other parameters but no transfer-syntax + assert_eq!( + extract_transfer_syntax_from_accept( + "multipart/related; type=\"application/dicom\"; boundary=example" + ), + None + ); + } + #[test] fn parse_rendered_query_params() { let uri = diff --git a/src/backend/dimse/cmove/movescu.rs b/src/backend/dimse/cmove/movescu.rs index 1bcc677..15b4ee4 100644 --- a/src/backend/dimse/cmove/movescu.rs +++ b/src/backend/dimse/cmove/movescu.rs @@ -87,6 +87,8 @@ pub enum MoveError { #[error(transparent)] Write(#[from] WriteError), #[error(transparent)] + Transcode(#[from] dicom::pixeldata::TranscodeError), + #[error(transparent)] Association(#[from] PoolError), #[error("Sub-operation failed")] OperationFailed, diff --git a/src/backend/dimse/wado.rs b/src/backend/dimse/wado.rs index 02b91a0..4cab123 100644 --- a/src/backend/dimse/wado.rs +++ b/src/backend/dimse/wado.rs @@ -17,7 +17,10 @@ use async_stream::stream; use async_trait::async_trait; use dicom::core::VR; use dicom::dictionary_std::tags; +use dicom::encoding::TransferSyntaxIndex; use dicom::object::{FileDicomObject, InMemDicomObject}; +use dicom::transfer_syntax::TransferSyntaxRegistry; +use dicom_pixeldata::Transcode; use futures::stream::BoxStream; use futures::{Stream, StreamExt}; use pin_project::pin_project; @@ -104,6 +107,7 @@ impl WadoService for DimseWadoService { async fn metadata(&self, request: MetadataRequest) -> Result { self.retrieve(RetrieveInstanceRequest { query: request.query, + transfer_syntax: None, }) .await } @@ -263,11 +267,23 @@ impl<'a> DicomMultipartStream<'a> { stream: impl Stream>, MoveError>> + Send + 'a, + transfer_syntax_uid: Option<&str>, ) -> Self { + let transfer_syntax_uid = + transfer_syntax_uid.and_then(|ts_uid| TransferSyntaxRegistry.get(ts_uid)); + let multipart_stream = stream - .map(|item| { + .map(move |item| { + let transfer_syntax_uid = transfer_syntax_uid; item.and_then(|object| { - Self::write(&object).map_err(|err| MoveError::Write(WriteError::Io(err))) + if let Some(ts) = transfer_syntax_uid { + let mut transcoded = (*object).clone(); + transcoded.transcode(ts).map_err(MoveError::Transcode)?; + Self::write(&transcoded) + .map_err(|err| MoveError::Write(WriteError::Io(err))) + } else { + Self::write(&object).map_err(|err| MoveError::Write(WriteError::Io(err))) + } }) }) .chain(futures::stream::once(async { @@ -289,7 +305,11 @@ impl<'a> DicomMultipartStream<'a> { let mut buffer = Vec::new(); writeln!(buffer, "--boundary\r")?; - writeln!(buffer, "Content-Type: application/dicom\r")?; + writeln!( + buffer, + "Content-Type: application/dicom; transfer-syntax=\"{}\"\r", + file.meta().transfer_syntax.trim_end_matches('\0') + )?; writeln!(buffer, "Content-Length: {file_length}\r")?; writeln!(buffer, "\r")?; buffer.append(&mut dcm);