Skip to content

Commit 4b987e4

Browse files
knopkemCopilot
andcommitted
Prepare v0.2.0 release
Add getscu, update release packaging, and keep the workspace MSRV-clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 260ea01 commit 4b987e4

21 files changed

Lines changed: 717 additions & 150 deletions

File tree

.github/workflows/release.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
- name: Strip binaries (Unix)
6363
if: runner.os != 'Windows' && !matrix.cross
6464
run: |
65-
for bin in dcmdump echoscu storescu storescp findscu img2dcm dcmcjpls dcmdjpls dcmcjp2k dcmdjp2k; do
65+
for bin in dcmdump echoscu storescu storescp findscu getscu img2dcm dcmcjpls dcmdjpls dcmcjp2k dcmdjp2k; do
6666
strip "target/${{ matrix.target }}/release/${bin}" 2>/dev/null || true
6767
done
6868
@@ -72,7 +72,7 @@ jobs:
7272
run: |
7373
STAGING="dicom-toolkit-${{ github.ref_name }}-${{ matrix.target }}"
7474
mkdir -p "${STAGING}"
75-
for bin in dcmdump echoscu storescu storescp findscu img2dcm dcmcjpls dcmdjpls dcmcjp2k dcmdjp2k; do
75+
for bin in dcmdump echoscu storescu storescp findscu getscu img2dcm dcmcjpls dcmdjpls dcmcjp2k dcmdjp2k; do
7676
cp "target/${{ matrix.target }}/release/${bin}" "${STAGING}/"
7777
done
7878
cp LICENSE-MIT LICENSE-APACHE NOTICE README.md "${STAGING}/"
@@ -86,7 +86,7 @@ jobs:
8686
run: |
8787
$staging = "dicom-toolkit-${{ github.ref_name }}-${{ matrix.target }}"
8888
New-Item -ItemType Directory -Path $staging
89-
$bins = @("dcmdump", "echoscu", "storescu", "storescp", "findscu", "img2dcm", "dcmcjpls", "dcmdjpls", "dcmcjp2k", "dcmdjp2k")
89+
$bins = @("dcmdump", "echoscu", "storescu", "storescp", "findscu", "getscu", "img2dcm", "dcmcjpls", "dcmdjpls", "dcmcjp2k", "dcmdjp2k")
9090
foreach ($bin in $bins) {
9191
Copy-Item "target/${{ matrix.target }}/release/${bin}.exe" $staging
9292
}
@@ -146,6 +146,7 @@ jobs:
146146
| `storescu` | DICOM C-STORE SCU (send files) |
147147
| `storescp` | DICOM C-STORE SCP (receive files) |
148148
| `findscu` | DICOM C-FIND SCU (query) |
149+
| `getscu` | DICOM C-GET SCU (retrieve files) |
149150
| `img2dcm` | Convert PNG images to DICOM |
150151
| `dcmcjpls` | Compress DICOM with JPEG-LS |
151152
| `dcmdjpls` | Decompress JPEG-LS DICOM files |

README.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A pure-Rust port of [DCMTK](https://dicom.offis.de/dcmtk.php.en) 3.7.0 — a com
88

99
This is an independent project, not affiliated with or endorsed by OFFIS e.V. See [NOTICE](NOTICE) for attribution details.
1010

11-
[![Tests](https://img.shields.io/badge/tests-487%20passing-brightgreen)](#status)
11+
[![Tests](https://img.shields.io/badge/tests-488%20passing-brightgreen)](#status)
1212

1313
---
1414

@@ -20,8 +20,8 @@ This is an independent project, not affiliated with or endorsed by OFFIS e.V. Se
2020
| 2 — Data model & I/O | `dicom-toolkit-data` | 153 |
2121
| 3 — Networking | `dicom-toolkit-net` | 59 |
2222
| 4 — Imaging & codecs | `dicom-toolkit-image`, `dicom-toolkit-codec`, `dicom-toolkit-jpeg2000` | 44 + 89 + 46 |
23-
| Tools | `dicom-toolkit-tools` | 9 integration |
24-
| **Total** | | **481 unit/integration + 6 doctests = 487 passing, 0 failed** |
23+
| Tools | `dicom-toolkit-tools` | 10 integration |
24+
| **Total** | | **482 unit/integration + 6 doctests = 488 passing, 0 failed** |
2525

2626
---
2727

@@ -35,7 +35,7 @@ This is an independent project, not affiliated with or endorsed by OFFIS e.V. Se
3535
| [`dicom-toolkit-net`](crates/dicom-toolkit-net) | `dcmnet`, `dcmtls` | Async DICOM networking: PDU layer, association, C-ECHO/STORE/FIND/GET/MOVE, TLS |
3636
| [`dicom-toolkit-image`](crates/dicom-toolkit-image) | `dcmimgle`, `dcmimage` | Pixel pipeline, Modality/VOI LUT, window/level, overlays, color models, PNG export |
3737
| [`dicom-toolkit-codec`](crates/dicom-toolkit-codec) | `dcmjpeg`, `dcmjpls`, `dcmrle`, `dcmjp2k` | JPEG baseline, **pure-Rust JPEG-LS**, **pure-Rust JPEG 2000** (lossless & lossy), RLE PackBits, codec registry |
38-
| [`dicom-toolkit-tools`](crates/dicom-toolkit-tools) | `dcmdump`, `echoscu`, etc. | CLI utilities: dump, network SCU/SCP, img2dcm, JPEG-LS/JPEG 2000 compress/decompress (see below) |
38+
| [`dicom-toolkit-tools`](crates/dicom-toolkit-tools) | `dcmdump`, `echoscu`, etc. | CLI utilities: dump, network SCU/SCP including `getscu`, img2dcm, JPEG-LS/JPEG 2000 compress/decompress (see below) |
3939
| [`dicom-toolkit-jpeg2000`](crates/dicom-toolkit-jpeg2000) | internal/published fork | Pure-Rust JPEG 2000 engine used by `dicom-toolkit-codec`; published fork with native-bit-depth decode plus DICOM-focused encoder |
4040

4141
---
@@ -329,6 +329,34 @@ findscu -L PATIENT -k "0010,0010=" -k "0008,0020=20240101" localhost 4242
329329

330330
---
331331

332+
### `getscu` — retrieve with C-GET
333+
334+
```
335+
getscu [OPTIONS] <HOST> <PORT>
336+
337+
Options:
338+
-a, --aetitle <AE> Calling AE title [default: GETSCU]
339+
-c, --called-ae <AE> Called AE title [default: ANY-SCP]
340+
-d, --output-dir <DIR> Directory for retrieved DICOM files [default: .]
341+
-k, --key <TAG=VALUE> Query key (repeatable)
342+
-L, --level <LEVEL> Query/retrieve level [default: STUDY]
343+
-v, --verbose Verbose output
344+
```
345+
346+
**Examples**
347+
348+
```bash
349+
# Retrieve all instances for a study into ./retrieved
350+
getscu -d retrieved -L STUDY -k "0020,000D=1.2.3.4.5" pacs.example.com 11112
351+
352+
# Retrieve matching series from a local PACS
353+
getscu -a MY_SCU -c ORTHANC -d out -L SERIES -k "0020,000E=1.2.3.4.5.6" localhost 4242
354+
```
355+
356+
Retrieved objects are written as DICOM Part 10 files named after their SOP Instance UID.
357+
358+
---
359+
332360
### `img2dcm` — convert PNG to DICOM
333361

334362
```
@@ -476,7 +504,7 @@ Ready-to-run scripts live in [`examples/scripts/`](examples/scripts/) and use th
476504
|--------|---------------------|
477505
| `01_dump` | All `dcmdump` output modes: plain, `--meta`, `--no-limit`, `--json`, `--xml`, multi-file batch |
478506
| `02_network` | Start `storescp` → C-ECHO verify with `echoscu` → send all 5 slices with `storescu` → inspect received files |
479-
| `03_query` | `findscu` command patterns; set `RUN_LIVE=1` / `$env:RUN_LIVE='1'` to query a real PACS |
507+
| `03_query` | `findscu` and `getscu` command patterns; set `RUN_LIVE=1` / `$env:RUN_LIVE='1'` to query a real PACS |
480508
| `04_img2dcm` | Generate a PNG with Python stdlib → wrap as Secondary Capture → dump + JSON export |
481509
| `05_jpegls` | JPEG-LS lossless & near-lossless round-trip, batch compress/decompress, metadata verification |
482510
| `06_jp2k` | JPEG 2000 lossless round-trip, lossy smoke test, batch compress/decompress, metadata verification |
@@ -524,7 +552,7 @@ pwsh -File examples/scripts/06_jp2k.ps1
524552

525553
The PowerShell scripts also work on macOS and Linux with [PowerShell Core](https://github.com/PowerShell/PowerShell).
526554

527-
> **Note:** `03_query` shows command-line patterns but does not execute live queries by default. The `storescp` binary now uses the `DicomServer` framework. C-FIND, C-GET, and C-MOVE SCP handling is available in-process via the library; see [DicomServer](#dicomserver) below. Set `RUN_LIVE=1` to use an external Orthanc instance with the query scripts.
555+
> **Note:** `03_query` shows command-line patterns but does not execute live retrievals by default. The `storescp` binary now uses the `DicomServer` framework. C-FIND, C-GET, and C-MOVE SCP handling is available in-process via the library; see [DicomServer](#dicomserver) below. Set `RUN_LIVE=1` to use an external Orthanc instance with the query scripts.
528556
529557
---
530558

crates/dicom-toolkit-jpeg2000/src/j2c/bitplane.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ fn decode_inner(
126126
true
127127
} else {
128128
// Only for cleanup pass.
129-
start_coding_pass.is_multiple_of(3)
129+
start_coding_pass % 3 == 0
130130
}
131131
} else {
132132
true

crates/dicom-toolkit-jpeg2000/src/j2c/build.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,14 @@ fn build_code_blocks(
220220
);
221221

222222
let start = storage.layers.len();
223-
storage.layers.extend(iter::repeat_n(
224-
Layer {
223+
storage.layers.extend(
224+
iter::repeat(Layer {
225225
// This will be updated once we actually read the
226226
// layer segments.
227227
segments: None,
228-
},
229-
tile.num_layers as usize,
230-
));
228+
})
229+
.take(tile.num_layers as usize),
230+
);
231231
let end = storage.layers.len();
232232

233233
storage.code_blocks.push(CodeBlock {

crates/dicom-toolkit-jpeg2000/src/j2c/fdwt.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ fn forward_lift_53(data: &mut [f32]) {
198198

199199
// Step 1: Predict (high-pass) — update odd samples
200200
// d(i) = x(2i+1) - floor((x(2i) + x(2i+2)) / 2)
201-
let last_even = if n.is_multiple_of(2) { n - 2 } else { n - 1 };
201+
let last_even = if n % 2 == 0 { n - 2 } else { n - 1 };
202202
for i in (1..n).step_by(2) {
203203
let left = data[i - 1];
204204
let right = if i + 1 < n {
@@ -344,7 +344,7 @@ mod tests {
344344
data[i] -= ((left + right) * 0.25 + 0.5).floor();
345345
}
346346
// Undo predict
347-
let last_even = if n.is_multiple_of(2) { n - 2 } else { n - 1 };
347+
let last_even = if n % 2 == 0 { n - 2 } else { n - 1 };
348348
for i in (1..n).step_by(2) {
349349
let left = data[i - 1];
350350
let right = if i + 1 < n {

crates/dicom-toolkit-jpeg2000/src/j2c/idwt.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ pub(crate) fn apply(
111111

112112
// Determine which buffer we should use first, such that the `coefficients`
113113
// array will always hold the final values.
114-
let mut use_scratch = decompositions.len().is_multiple_of(2);
114+
let mut use_scratch = decompositions.len() % 2 == 0;
115115

116116
let mut temp_output = filter_2d(
117117
IDWTInput::from_sub_band(ll_sub_band, storage),
@@ -359,7 +359,7 @@ fn filter_horizontal(coefficients: &mut [f32], rect: IntRect, transform: Wavelet
359359
/// The `1D_SR` procedure from F.3.6.
360360
fn filter_row(scanline: &mut [f32], width: usize, x0: usize, transform: WaveletTransform) {
361361
if width == 1 {
362-
if !x0.is_multiple_of(2) {
362+
if x0 % 2 != 0 {
363363
scanline[0] *= 0.5;
364364
}
365365

@@ -575,7 +575,7 @@ fn filter_vertical_impl<S: Simd>(
575575
let y0 = rect.y0 as usize;
576576

577577
if height == 1 {
578-
if !y0.is_multiple_of(2) {
578+
if y0 % 2 != 0 {
579579
let simd_width = width / SIMD_WIDTH * SIMD_WIDTH;
580580
for base_column in (0..simd_width).step_by(SIMD_WIDTH) {
581581
let mut loaded = f32x8::from_slice(simd, &scanline[base_column..][..SIMD_WIDTH]);

crates/dicom-toolkit-jpeg2000/src/j2c/tile.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -603,16 +603,12 @@ impl<'a> ResolutionTile<'a> {
603603
// is divisible, then we can't take the x/y position of the tile
604604
// as the start of the precinct, but instead have to advance to the
605605
// next multiple.
606-
if !r_x.is_multiple_of(precinct_x_step)
607-
&& (self.rect.x0 * (1 << nl_minus_r)).is_multiple_of(precinct_x_step)
608-
{
606+
if r_x % precinct_x_step != 0 && (self.rect.x0 * (1 << nl_minus_r)) % precinct_x_step == 0 {
609607
r_x = r_x.checked_next_multiple_of(precinct_x_step)?;
610608
}
611609

612610
// Same as above.
613-
if !r_y.is_multiple_of(precinct_y_step)
614-
&& (self.rect.y0 * (1 << nl_minus_r)).is_multiple_of(precinct_y_step)
615-
{
611+
if r_y % precinct_y_step != 0 && (self.rect.y0 * (1 << nl_minus_r)) % precinct_y_step == 0 {
616612
r_y = r_y.checked_next_multiple_of(precinct_y_step)?;
617613
}
618614

crates/dicom-toolkit-jpeg2000/src/reader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ impl<'a> BitReader<'a> {
1919
pub(crate) fn align(&mut self) {
2020
let bit_pos = self.bit_pos();
2121

22-
if !bit_pos.is_multiple_of(8) {
22+
if bit_pos % 8 != 0 {
2323
self.cur_pos += 8 - bit_pos;
2424
}
2525
}

crates/dicom-toolkit-net/src/services/get.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ pub struct ReceivedInstance {
5252
pub sop_class_uid: String,
5353
/// SOP Instance UID of the received instance.
5454
pub sop_instance_uid: String,
55+
/// Transfer Syntax UID negotiated for the C-STORE sub-operation that
56+
/// delivered this dataset.
57+
pub transfer_syntax_uid: String,
5558
/// Raw encoded dataset bytes (use `DicomReader::read_dataset` to decode).
5659
pub dataset: Vec<u8>,
5760
}
@@ -114,11 +117,16 @@ pub async fn c_get(assoc: &mut Association, req: GetRequest) -> DcmResult<GetRes
114117
.trim_end_matches('\0')
115118
.to_string();
116119
let store_msg_id = rsp_cmd.get_u16(tags::MESSAGE_ID).unwrap_or(1);
120+
let transfer_syntax_uid = assoc
121+
.context_by_id(ctx_id)
122+
.map(|pc| pc.transfer_syntax.trim_end_matches('\0').to_string())
123+
.unwrap_or_else(|| TS_EXPLICIT_LE.to_string());
117124

118125
let data = assoc.recv_dimse_data().await?;
119126
instances.push(ReceivedInstance {
120127
sop_class_uid: sop_class.clone(),
121128
sop_instance_uid: sop_instance.clone(),
129+
transfer_syntax_uid,
122130
dataset: data,
123131
});
124132

crates/dicom-toolkit-net/tests/e2e.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,18 @@ async fn test_get_loopback() {
448448
assert_eq!(result.instances.len(), 2, "should receive 2 instances");
449449
assert_eq!(result.instances[0].sop_instance_uid, "1.2.3.101");
450450
assert_eq!(result.instances[1].sop_instance_uid, "1.2.3.102");
451+
assert_eq!(
452+
result.instances[0]
453+
.transfer_syntax_uid
454+
.trim_end_matches('\0'),
455+
TS_EXPLICIT_LE
456+
);
457+
assert_eq!(
458+
result.instances[1]
459+
.transfer_syntax_uid
460+
.trim_end_matches('\0'),
461+
TS_EXPLICIT_LE
462+
);
451463

452464
let ds0 = decode_dataset(&result.instances[0].dataset);
453465
let ds1 = decode_dataset(&result.instances[1].dataset);

0 commit comments

Comments
 (0)