Skip to content

Commit cff2fb4

Browse files
authored
Merge pull request #44 from mbits-imaging/transcoding
feat(transcode): support transcoding
2 parents 2a2b619 + c7759c2 commit cff2fb4

4 files changed

Lines changed: 126 additions & 6 deletions

File tree

src/api/wado/routes.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ async fn instance_resource(
7070
request: RetrieveInstanceRequest,
7171
) -> impl IntoResponse {
7272
if let Some(wado) = provider.wado {
73+
let transfer_syntax = request.transfer_syntax.clone();
7374
let study_instance_uid: UI = request.query.study_instance_uid.clone();
7475
let response = wado.retrieve(request).await;
7576

@@ -92,6 +93,7 @@ async fn instance_resource(
9293
)
9394
.body(Body::from_stream(DicomMultipartStream::new(
9495
stream.into_stream(),
96+
transfer_syntax.as_deref(),
9597
)))
9698
.unwrap()
9799
}
@@ -131,6 +133,7 @@ async fn rendered_resource(
131133
trace!("Using default rendering");
132134
let instance_request = RetrieveInstanceRequest {
133135
query: request.query,
136+
transfer_syntax: None,
134137
};
135138

136139
let stream = wado

src/api/wado/service.rs

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ pub enum RetrieveError {
5252
impl IntoResponse for RetrieveError {
5353
fn into_response(self) -> Response {
5454
match self {
55-
RetrieveError::Backend { source } => {
55+
Self::Backend { source } => {
5656
(StatusCode::INTERNAL_SERVER_ERROR, source.to_string()).into_response()
5757
}
58-
RetrieveError::Unimplemented => Response::builder()
58+
Self::Unimplemented => Response::builder()
5959
.status(StatusCode::NOT_IMPLEMENTED)
6060
.body(Body::from("This transaction is not implemented."))
6161
.unwrap(),
@@ -64,6 +64,7 @@ impl IntoResponse for RetrieveError {
6464
}
6565
pub struct RetrieveInstanceRequest {
6666
pub query: ResourceQuery,
67+
pub transfer_syntax: Option<String>,
6768
}
6869

6970
pub struct ThumbnailRequest {
@@ -157,6 +158,33 @@ where
157158
}
158159
}
159160

161+
/// Extracts the transfer-syntax parameter from an Accept header value.
162+
///
163+
/// According to <https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.3.5.2.html>
164+
/// the syntax is: transfer-syntax-mtp = OWS ";" OWS %s"transfer-syntax=" ts-value
165+
///
166+
/// Examples:
167+
/// - "application/dicom; transfer-syntax=1.2.840.10008.1.2.4.50"
168+
/// - "multipart/related; type=\"application/dicom\"; transfer-syntax=1.2.840.10008.1.2.4.50"
169+
fn extract_transfer_syntax_from_accept(accept_header: &str) -> Option<String> {
170+
// Split by semicolons to get individual parameters
171+
for part in accept_header.split(';') {
172+
let trimmed = part.trim();
173+
// Look for the transfer-syntax parameter
174+
if let Some(value) = trimmed.strip_prefix("transfer-syntax=") {
175+
let value = value.trim();
176+
// The value might be quoted or unquoted
177+
let transfer_syntax = if value.starts_with('"') && value.ends_with('"') {
178+
value.trim_matches('"')
179+
} else {
180+
value
181+
};
182+
return Some(transfer_syntax.to_string());
183+
}
184+
}
185+
None
186+
}
187+
160188
impl<S> FromRequestParts<S> for RetrieveInstanceRequest
161189
where
162190
AppState: FromRef<S>,
@@ -169,7 +197,21 @@ where
169197
.await
170198
.map_err(PathRejection::into_response)?;
171199

172-
Ok(Self { query })
200+
let accept = parts
201+
.headers
202+
.get(ACCEPT)
203+
.map(|h| String::from(h.to_str().unwrap_or_default()));
204+
205+
// Extract the requested transfer-syntax from the Accept header if present
206+
// https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.3.5.2.html
207+
let transfer_syntax = accept
208+
.as_ref()
209+
.and_then(|accept_str| extract_transfer_syntax_from_accept(accept_str));
210+
211+
Ok(Self {
212+
query,
213+
transfer_syntax,
214+
})
173215
}
174216
}
175217

@@ -456,6 +498,59 @@ mod tests {
456498
);
457499
}
458500

501+
#[test]
502+
fn test_extract_transfer_syntax_from_accept() {
503+
// Test simple case
504+
assert_eq!(
505+
extract_transfer_syntax_from_accept(
506+
"application/dicom; transfer-syntax=1.2.840.10008.1.2.4.50"
507+
),
508+
Some("1.2.840.10008.1.2.4.50".to_string())
509+
);
510+
511+
// Test with extra whitespace
512+
assert_eq!(
513+
extract_transfer_syntax_from_accept(
514+
"application/dicom; transfer-syntax=1.2.840.10008.1.2.4.50"
515+
),
516+
Some("1.2.840.10008.1.2.4.50".to_string())
517+
);
518+
519+
// Test wildcard
520+
assert_eq!(
521+
extract_transfer_syntax_from_accept("application/dicom; transfer-syntax=*"),
522+
Some("*".to_string())
523+
);
524+
525+
// Test multipart/related with type parameter
526+
assert_eq!(
527+
extract_transfer_syntax_from_accept("multipart/related; type=\"application/dicom\"; transfer-syntax=1.2.840.10008.1.2.4.50"),
528+
Some("1.2.840.10008.1.2.4.50".to_string())
529+
);
530+
531+
// Test with quoted value (though not typical for transfer-syntax)
532+
assert_eq!(
533+
extract_transfer_syntax_from_accept(
534+
"application/dicom; transfer-syntax=\"1.2.840.10008.1.2.4.50\""
535+
),
536+
Some("1.2.840.10008.1.2.4.50".to_string())
537+
);
538+
539+
// Test without transfer-syntax parameter
540+
assert_eq!(
541+
extract_transfer_syntax_from_accept("application/dicom"),
542+
None
543+
);
544+
545+
// Test with other parameters but no transfer-syntax
546+
assert_eq!(
547+
extract_transfer_syntax_from_accept(
548+
"multipart/related; type=\"application/dicom\"; boundary=example"
549+
),
550+
None
551+
);
552+
}
553+
459554
#[test]
460555
fn parse_rendered_query_params() {
461556
let uri =

src/backend/dimse/cmove/movescu.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ pub enum MoveError {
8787
#[error(transparent)]
8888
Write(#[from] WriteError),
8989
#[error(transparent)]
90+
Transcode(#[from] dicom::pixeldata::TranscodeError),
91+
#[error(transparent)]
9092
Association(#[from] PoolError<AssociationError>),
9193
#[error("Sub-operation failed")]
9294
OperationFailed,

src/backend/dimse/wado.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ use async_stream::stream;
1717
use async_trait::async_trait;
1818
use dicom::core::VR;
1919
use dicom::dictionary_std::tags;
20+
use dicom::encoding::TransferSyntaxIndex;
2021
use dicom::object::{FileDicomObject, InMemDicomObject};
22+
use dicom::transfer_syntax::TransferSyntaxRegistry;
23+
use dicom_pixeldata::Transcode;
2124
use futures::stream::BoxStream;
2225
use futures::{Stream, StreamExt};
2326
use pin_project::pin_project;
@@ -104,6 +107,7 @@ impl WadoService for DimseWadoService {
104107
async fn metadata(&self, request: MetadataRequest) -> Result<InstanceResponse, RetrieveError> {
105108
self.retrieve(RetrieveInstanceRequest {
106109
query: request.query,
110+
transfer_syntax: None,
107111
})
108112
.await
109113
}
@@ -263,11 +267,23 @@ impl<'a> DicomMultipartStream<'a> {
263267
stream: impl Stream<Item = Result<Arc<FileDicomObject<InMemDicomObject>>, MoveError>>
264268
+ Send
265269
+ 'a,
270+
transfer_syntax_uid: Option<&str>,
266271
) -> Self {
272+
let transfer_syntax_uid =
273+
transfer_syntax_uid.and_then(|ts_uid| TransferSyntaxRegistry.get(ts_uid));
274+
267275
let multipart_stream = stream
268-
.map(|item| {
276+
.map(move |item| {
277+
let transfer_syntax_uid = transfer_syntax_uid;
269278
item.and_then(|object| {
270-
Self::write(&object).map_err(|err| MoveError::Write(WriteError::Io(err)))
279+
if let Some(ts) = transfer_syntax_uid {
280+
let mut transcoded = (*object).clone();
281+
transcoded.transcode(ts).map_err(MoveError::Transcode)?;
282+
Self::write(&transcoded)
283+
.map_err(|err| MoveError::Write(WriteError::Io(err)))
284+
} else {
285+
Self::write(&object).map_err(|err| MoveError::Write(WriteError::Io(err)))
286+
}
271287
})
272288
})
273289
.chain(futures::stream::once(async {
@@ -289,7 +305,11 @@ impl<'a> DicomMultipartStream<'a> {
289305
let mut buffer = Vec::new();
290306

291307
writeln!(buffer, "--boundary\r")?;
292-
writeln!(buffer, "Content-Type: application/dicom\r")?;
308+
writeln!(
309+
buffer,
310+
"Content-Type: application/dicom; transfer-syntax=\"{}\"\r",
311+
file.meta().transfer_syntax.trim_end_matches('\0')
312+
)?;
293313
writeln!(buffer, "Content-Length: {file_length}\r")?;
294314
writeln!(buffer, "\r")?;
295315
buffer.append(&mut dcm);

0 commit comments

Comments
 (0)