Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/api/wado/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -92,6 +93,7 @@ async fn instance_resource(
)
.body(Body::from_stream(DicomMultipartStream::new(
stream.into_stream(),
transfer_syntax.as_deref(),
)))
.unwrap()
}
Expand Down Expand Up @@ -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
Expand Down
101 changes: 98 additions & 3 deletions src/api/wado/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -64,6 +64,7 @@ impl IntoResponse for RetrieveError {
}
pub struct RetrieveInstanceRequest {
pub query: ResourceQuery,
pub transfer_syntax: Option<String>,
}

pub struct ThumbnailRequest {
Expand Down Expand Up @@ -157,6 +158,33 @@ where
}
}

/// Extracts the transfer-syntax parameter from an Accept header value.
///
/// According to <https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.3.5.2.html>
/// 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<String> {
// 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<S> FromRequestParts<S> for RetrieveInstanceRequest
where
AppState: FromRef<S>,
Expand All @@ -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,
})
}
}

Expand Down Expand Up @@ -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 =
Expand Down
2 changes: 2 additions & 0 deletions src/backend/dimse/cmove/movescu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ pub enum MoveError {
#[error(transparent)]
Write(#[from] WriteError),
#[error(transparent)]
Transcode(#[from] dicom::pixeldata::TranscodeError),
#[error(transparent)]
Association(#[from] PoolError<AssociationError>),
#[error("Sub-operation failed")]
OperationFailed,
Expand Down
26 changes: 23 additions & 3 deletions src/backend/dimse/wado.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -104,6 +107,7 @@ impl WadoService for DimseWadoService {
async fn metadata(&self, request: MetadataRequest) -> Result<InstanceResponse, RetrieveError> {
self.retrieve(RetrieveInstanceRequest {
query: request.query,
transfer_syntax: None,
})
.await
}
Expand Down Expand Up @@ -263,11 +267,23 @@ impl<'a> DicomMultipartStream<'a> {
stream: impl Stream<Item = Result<Arc<FileDicomObject<InMemDicomObject>>, 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 {
Expand All @@ -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);
Expand Down
Loading