Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
97 changes: 96 additions & 1 deletion src/api/wado/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
}
pub struct RetrieveInstanceRequest {
pub query: ResourceQuery,
pub transfer_syntax: Option<String>,
}

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

/// 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
Comment thread Fixed
Comment thread Fixed
/// 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 @@
.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 @@
);
}

#[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
32 changes: 28 additions & 4 deletions src/backend/dimse/wado.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
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 @@
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,26 @@
stream: impl Stream<Item = Result<Arc<FileDicomObject<InMemDicomObject>>, MoveError>>
+ Send
+ 'a,
transfer_syntax_uid: Option<&str>,
) -> Self {
let transfer_syntax_uid = transfer_syntax_uid
.map(|ts_uid| TransferSyntaxRegistry.get(&ts_uid))
Comment thread Fixed
Comment thread Fixed
.flatten();
Comment thread Fixed
Comment thread Fixed

let multipart_stream = stream
.map(|item| {
.map(move |item| {
let transfer_syntax_uid = transfer_syntax_uid.clone();
Comment thread Fixed
Comment thread Fixed
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)
Comment thread Fixed
Comment thread Fixed
.map_err(|err| MoveError::Transcode(err))?;
Comment thread Fixed
Comment thread Fixed
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 @@ -276,7 +295,7 @@
.boxed();

Self {
inner: multipart_stream,

Check warning

Code scanning / clippy

variables can be used directly in the format! string Warning

variables can be used directly in the format! string

Check warning

Code scanning / clippy

variables can be used directly in the format! string Warning

variables can be used directly in the format! string
}
}

Expand All @@ -289,8 +308,13 @@
let mut buffer = Vec::new();

writeln!(buffer, "--boundary\r")?;
writeln!(buffer, "Content-Type: application/dicom\r")?;
writeln!(buffer, "Content-Length: {file_length}\r")?;
writeln!(
buffer,
"Content-Type: {}; transfer-syntax=\"{}\"\r",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transfer syntax UIDs might have a trailing null character (\0), which trim does not remove. So it might be worth looking for places where these null characters can emerge by mistake in the output (there was something in the first screenshot).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a commit trimming these trailings null in this place. I think in this PR this is the only relevant place

"application/dicom",
Comment thread Fixed
Comment thread Fixed
file.meta().transfer_syntax.trim_end_matches('\0')
)?;
writeln!(buffer, "Content-Length: {}\r", file_length)?;
writeln!(buffer, "\r")?;
buffer.append(&mut dcm);
writeln!(buffer, "\r")?;
Expand Down
Loading