├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── clippy.toml ├── core ├── .gitignore ├── Cargo.toml ├── README.md ├── src │ ├── dictionary │ │ ├── data_element.rs │ │ ├── mod.rs │ │ ├── stub.rs │ │ └── uid.rs │ ├── header.rs │ ├── lib.rs │ ├── ops.rs │ ├── prelude.rs │ └── value │ │ ├── deserialize.rs │ │ ├── fragments.rs │ │ ├── mod.rs │ │ ├── partial.rs │ │ ├── person_name.rs │ │ ├── primitive.rs │ │ ├── range.rs │ │ └── serialize.rs └── tests │ ├── dicom_value.rs │ └── using_prelude.rs ├── devtools └── dictionary-builder │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── common.rs │ ├── main.rs │ ├── tags.rs │ └── uids.rs ├── dictionary-std ├── Cargo.toml ├── README.md └── src │ ├── data_element.rs │ ├── lib.rs │ ├── sop_class.rs │ ├── tags.rs │ └── uids.rs ├── dump ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── main.rs ├── echoscu ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── encoding ├── Cargo.toml ├── README.md └── src │ ├── adapters.rs │ ├── decode │ ├── basic.rs │ ├── explicit_be.rs │ ├── explicit_le.rs │ ├── implicit_le.rs │ └── mod.rs │ ├── encode │ ├── basic.rs │ ├── explicit_be.rs │ ├── explicit_le.rs │ ├── implicit_le.rs │ └── mod.rs │ ├── lib.rs │ ├── text.rs │ └── transfer_syntax │ └── mod.rs ├── findscu ├── Cargo.toml ├── README.md └── src │ ├── main.rs │ └── query.rs ├── fromimage ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── json ├── Cargo.toml ├── README.md └── src │ ├── de │ ├── mod.rs │ └── value.rs │ ├── lib.rs │ └── ser │ ├── mod.rs │ └── value.rs ├── object ├── Cargo.toml ├── README.md ├── fuzz │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── fuzz.sh │ └── fuzz_targets │ │ └── open_file.rs ├── src │ ├── attribute.rs │ ├── file.rs │ ├── lib.rs │ ├── mem.rs │ ├── meta.rs │ ├── ops.rs │ └── tokens.rs └── tests │ └── integration_test.rs ├── parent ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── parser ├── Cargo.toml ├── README.md └── src │ ├── dataset │ ├── lazy_read.rs │ ├── mod.rs │ ├── read.rs │ └── write.rs │ ├── lib.rs │ ├── stateful │ ├── decode.rs │ ├── encode.rs │ └── mod.rs │ └── util.rs ├── pixeldata ├── Cargo.toml ├── README.md ├── fuzz │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── fuzz.sh │ └── fuzz_targets │ │ ├── decode_image_file.rs │ │ └── decode_simple_image.rs ├── src │ ├── attribute.rs │ ├── bin │ │ └── dicom-transcode.rs │ ├── encapsulation.rs │ ├── gdcm.rs │ ├── lib.rs │ ├── lut.rs │ ├── transcode.rs │ └── transform.rs └── tests │ └── image_reader.rs ├── scpproxy ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── storescp ├── Cargo.toml ├── README.md └── src │ ├── main.rs │ ├── store_async.rs │ ├── store_sync.rs │ └── transfer.rs ├── storescu ├── Cargo.toml ├── README.md ├── out.json └── src │ ├── main.rs │ ├── store_async.rs │ └── store_sync.rs ├── toimage ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── transfer-syntax-registry ├── Cargo.toml ├── README.md ├── src │ ├── adapters │ │ ├── jpeg.rs │ │ ├── jpeg2k.rs │ │ ├── jpegls.rs │ │ ├── jpegxl.rs │ │ ├── mod.rs │ │ ├── rle_lossless.rs │ │ └── uncompressed.rs │ ├── deflate.rs │ ├── entries.rs │ └── lib.rs └── tests │ ├── adapters │ └── mod.rs │ ├── base.rs │ ├── jpeg.rs │ ├── jpegls.rs │ ├── jpegxl.rs │ ├── rle.rs │ ├── submit_dataset.rs │ ├── submit_pixel.rs │ ├── submit_pixel_stub.rs │ ├── submit_replace.rs │ └── submit_replace_precondition.rs └── ul ├── Cargo.toml ├── README.md ├── fuzz ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── fuzz.sh └── fuzz_targets │ └── pdu_roundtrip.rs ├── src ├── address.rs ├── association │ ├── client.rs │ ├── mod.rs │ ├── pdata.rs │ ├── server.rs │ └── uid.rs ├── lib.rs └── pdu │ ├── mod.rs │ ├── reader.rs │ └── writer.rs ├── test.json └── tests ├── association.rs ├── association_echo.rs ├── association_promiscuous.rs ├── association_store.rs ├── association_store_uncompressed.rs └── pdu.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: continuous integration 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | test: 12 | name: Test (default) 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | rust: 17 | - 1.72.0 18 | - stable 19 | - beta 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions-rust-lang/setup-rust-toolchain@v1 23 | with: 24 | toolchain: ${{ matrix.rust }} 25 | components: clippy 26 | cache: true 27 | # test project with default + extra features 28 | - if: matrix.rust == 'stable' || matrix.rust == 'beta' 29 | run: cargo test --features image,ndarray,sop-class,rle,cli,jpegxl 30 | # test dicom-pixeldata with openjp2 31 | - if: matrix.rust == 'stable' || matrix.rust == 'beta' 32 | run: cargo test -p dicom-pixeldata --features openjp2 33 | # test dicom-pixeldata with openjpeg-sys and charls 34 | - if: matrix.rust == 'stable' || matrix.rust == 'beta' 35 | run: cargo test -p dicom-pixeldata --features openjpeg-sys,charls 36 | # test dicom-pixeldata with gdcm-rs 37 | - if: matrix.rust == 'stable' || matrix.rust == 'beta' 38 | run: cargo test -p dicom-pixeldata --features gdcm 39 | # test dicom-pixeldata without default features 40 | - if: matrix.rust == 'stable' || matrix.rust == 'beta' 41 | run: cargo test -p dicom-pixeldata --no-default-features 42 | # test dicom-ul with async feature 43 | - if: matrix.rust == 'stable' || matrix.rust == 'beta' 44 | run: cargo test -p dicom-ul --features async 45 | # test library projects with minimum rust version 46 | - if: matrix.rust == '1.72.0' 47 | run: | 48 | cargo test -p dicom-core 49 | cargo test -p dicom-encoding 50 | cargo test -p dicom-dictionary-std 51 | cargo test -p dicom-parser 52 | cargo test -p dicom-transfer-syntax-registry 53 | cargo test -p dicom-object 54 | cargo test -p dicom-dump --no-default-features --features sop-class 55 | cargo test -p dicom-json 56 | cargo test -p dicom-ul 57 | cargo test -p dicom-pixeldata 58 | cargo check -p dicom 59 | env: 60 | RUSTFLAGS: -A warnings # allows warnings, to not pollute CI with warnings that are no longer valid with stable rust version 61 | # run Clippy with stable toolchain 62 | - if: matrix.rust == 'stable' 63 | run: cargo clippy 64 | env: 65 | RUSTFLAGS: -D warnings 66 | 67 | check_windows: 68 | name: Build (Windows) 69 | runs-on: windows-latest 70 | steps: 71 | - uses: actions/checkout@v4 72 | - uses: actions-rust-lang/setup-rust-toolchain@v1 73 | with: 74 | toolchain: stable 75 | cache: true 76 | - run: cargo build --features=cli,inventory-registry,sop-class 77 | 78 | check_macos: 79 | name: Check (macOS) 80 | runs-on: macos-latest 81 | steps: 82 | - uses: actions/checkout@v4 83 | - uses: actions-rust-lang/setup-rust-toolchain@v1 84 | with: 85 | toolchain: stable 86 | cache: true 87 | - run: cargo check 88 | 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /rls/ 2 | .vscode/ 3 | target/ 4 | *.rs.bk 5 | .idea/ 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 5 | 6 | ## Our Standards 7 | Examples of behavior that contributes to creating a positive environment include: 8 | 9 | - Using welcoming and inclusive language 10 | - Being respectful of differing viewpoints and experiences 11 | - Gracefully accepting constructive criticism 12 | - Focusing on what is best for the community 13 | - Showing empathy towards other community members 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 18 | - Trolling, insulting/derogatory comments, and personal or political attacks 19 | - Public or private harassment 20 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 21 | - Other conduct which could reasonably be considered inappropriate in a professional setting 22 | 23 | ## Our Responsibilities 24 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 25 | 26 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | ## Scope 29 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 30 | 31 | ## Enforcement 32 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. 33 | The current single project team member of the project is Eduardo Pinho: . 34 | All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 35 | 36 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 37 | 38 | ## Attribution 39 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 40 | 41 | For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DICOM-rs 2 | 3 | The DICOM-rs project is open for external contributions 4 | in the form of issues, pull requests, and otherwise constructive discussion 5 | about design concerns and future perspectives of the project. 6 | Although this project combines two major domains of expertize, 7 | namely the DICOM standard and the Rust programming language, 8 | it is acceptable for a contributor of DICOM-rs 9 | to only possess basic knowledge in either one. 10 | 11 | Please do not forget to follow the [Code of Conduct](CODE_OF_CONDUCT.md) 12 | in all interactions through the given project communication venues. 13 | 14 | ## Contributing with code 15 | 16 | Please check out the list of existing issues in the [GitHub issue tracker]. 17 | Should you be interested in helping but not know where to begin, 18 | please look for issues tagged `help wanted` and `good first issue`. 19 | Announcing your interest in pursuing an issue is recommended. 20 | This will prevent people from concurrently working on the same issue, 21 | and you may also receive some guidance along the way. 22 | No need to be shy! 23 | 24 | Pull requests are likely to be subjected to constructive and careful reviewing, 25 | and it may take some time before they are accepted and merged. 26 | Please do not be discouraged to contribute when not facing the expected outcome, 27 | or feeling that your work or proposal goes unheard. 28 | The project is maintained by volunteers outside of work hours. 29 | 30 | [GitHub issue tracker]: https://github.com/Enet4/dicom-rs/issues 31 | 32 | ### Building the project 33 | As a pure Rust ecosystem, 34 | Cargo is the main tool for building all crates in DICOM-rs. 35 | [Rustup] is the recommended way to set up a development environment 36 | for working with Rust. 37 | DICOM-rs expects the latest stable toolchain. 38 | 39 | Currently, all crates are gathered in the same workspace, 40 | which means that running the command below 41 | at the root of the repository will build all crates: 42 | 43 | ```sh 44 | cargo build 45 | ``` 46 | 47 | This will also build the CLI and helper tools of the project, 48 | such as the dictionary builder and `dcmdump`. 49 | To build only the library crates, 50 | you can build the parent package named `dicom`: 51 | 52 | ```sh 53 | cargo build -p dicom 54 | ``` 55 | 56 | Please ensure that all tests pass before sending your contribution. 57 | Writing tests for your own contributions is greatly appreciated as well. 58 | 59 | ```sh 60 | cargo test 61 | ``` 62 | 63 | We also recommend formatting your code before submitting, 64 | to prevent discrepancies in code style. 65 | 66 | ```sh 67 | cargo fmt 68 | ``` 69 | 70 | [Rustup]: https://rustup.rs 71 | 72 | ## Discussion and roadmapping 73 | If you have more long-termed ideas about what DICOM-rs should include next, 74 | please have a look at the [roadmap] and look into existing issues to provide feedback. 75 | You can also talk about the project at the official [DICOM-rs Zulip organization][zulip]. 76 | 77 | If you have any further questions or concerns, 78 | or would like to be deeper involved in the project, 79 | please reach out to the project maintainers. 80 | 81 | [roadmap]: https://github.com/Enet4/dicom-rs/wiki/Roadmap 82 | [zulip]: https://dicom-rs.zulipchat.com 83 | 84 | ## Project team and governance 85 | DICOM-rs is currently led by Eduardo Pinho ([**@Enet4**](https://github.com/Enet4), ). 86 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "core", 4 | "encoding", 5 | "parser", 6 | "transfer-syntax-registry", 7 | "object", 8 | "devtools/dictionary-builder", 9 | "dictionary-std", 10 | "dump", 11 | "echoscu", 12 | "encoding", 13 | "findscu", 14 | "fromimage", 15 | "json", 16 | "object", 17 | "parent", 18 | "parser", 19 | "pixeldata", 20 | "scpproxy", 21 | "storescp", 22 | "storescu", 23 | "toimage", 24 | "transfer-syntax-registry", 25 | "ul", 26 | ] 27 | 28 | # use edition 2021 resolver 29 | resolver = "2" 30 | 31 | # optimize JPEG decoder to run tests faster 32 | [profile.dev.package."jpeg-decoder"] 33 | opt-level = 2 34 | 35 | # optimize JPEG 2000 decoder to run tests faster 36 | [profile.dev.package.jpeg2k] 37 | opt-level = 2 38 | 39 | # optimize flate2 to run tests faster 40 | [profile.dev.package."flate2"] 41 | opt-level = 2 42 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Eduardo Pinho 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs 2 | 3 | [![DICOM-rs on crates.io](https://img.shields.io/crates/v/dicom.svg)](https://crates.io/crates/dicom) 4 | [![continuous integration](https://github.com/Enet4/dicom-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/Enet4/dicom-rs/actions/workflows/rust.yml) 5 | ![Minimum Rust Version Stable](https://img.shields.io/badge/Minimum%20Rust%20Version-stable-green.svg) 6 | [![dependency status](https://deps.rs/repo/github/Enet4/dicom-rs/status.svg)](https://deps.rs/repo/github/Enet4/dicom-rs) 7 | [![DICOM-rs chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://dicom-rs.zulipchat.com/) 8 | [![Documentation](https://docs.rs/dicom/badge.svg)](https://docs.rs/dicom) 9 | 10 | An ecosystem of library and tools for [DICOM](https://dicomstandard.org) compliant systems. 11 | 12 | This collection provides a pure Rust implementation of the DICOM standard, 13 | allowing users to work with DICOM objects 14 | and interact with DICOM applications, 15 | while aiming to be fast, safe, and intuitive to use. 16 | 17 | ## Components 18 | 19 | ### Library 20 | 21 | The following library packages are designed to be used in 22 | other Rust libraries and applications. 23 | 24 | - [`object`](object) provides a high-level abstraction of DICOM objects 25 | and functions for reading and writing DICOM files. 26 | - [`pixeldata`](pixeldata) enables the decoding and conversion of DICOM objects 27 | into usable imaging data structures, 28 | such as images and multidimensional arrays. 29 | - [`dump`](dump) provides helpful routines for 30 | dumping the contents of DICOM objects. 31 | - [`json`](json) provides serialization and deserialization to DICOM JSON. 32 | - [`ul`](ul) implements the DICOM upper layer protocol. 33 | - [`dictionary-std`](dictionary-std) contains a Rust definition of 34 | the standard data dictionary. 35 | - [`transfer-syntax-registry`](transfer-syntax-registry) contains a registry of 36 | transfer syntax specifications. 37 | - [`parser`](parser) provides a middle-level abstraction 38 | for the parsing and printing of DICOM data sets. 39 | - [`encoding`](encoding) contains DICOM data encoding and decoding primitives. 40 | - [`core`](core) represents all of the base traits, 41 | data structures and functions related to DICOM content. 42 | 43 | #### Using as a library 44 | 45 | The parent crate [`dicom`](parent) aggregates the key components of the full library, 46 | so it can be added to a project as an alternative to 47 | selectively grabbing the components that you need. 48 | 49 | Generally, most projects would add [`dicom_object`](object), 50 | which is the most usable crate for reading DICOM objects from a file or a similar source. 51 | This crate is available in `dicom::object`. 52 | For working with the imaging data of a DICOM object, 53 | add [`pixeldata`](pixeldata). 54 | Network capabilities may be constructed on top of [`ul`](ul). 55 | 56 | A simple example of use follows. 57 | For more details, 58 | please visit the [`dicom` documentation](https://docs.rs/dicom). 59 | 60 | ```rust 61 | use dicom::object::open_file; 62 | use dicom::dictionary_std::tags; 63 | 64 | let obj = open_file("0001.dcm")?; 65 | let patient_name = obj.element(tags::PATIENT_NAME)?.to_str()?; 66 | let modality = obj.element(tags::MODALITY)?.to_str()?; 67 | ``` 68 | 69 | ### Tools 70 | 71 | The project also comprises an assortment of command line tools. 72 | 73 | - [`dump`](dump), aside from being a library, 74 | is also a command-line application for inspecting DICOM files. 75 | - [`scpproxy`](scpproxy) implements a Proxy service class provider. 76 | - [`echoscu`](echoscu) implements a Verification service class user. 77 | - [`findscu`](findscu) implements a Find service class user. 78 | - [`storescu`](storescu) implements a Storage service class user. 79 | - [`storescp`](storescp) implements a Storage service class provider. 80 | - [`toimage`](toimage) lets you convert a DICOM file into an image file. 81 | - [`fromimage`](fromimage) lets you replace the imaging data of a DICOM file 82 | with one from an image file. 83 | - [`pixeldata`](pixeldata) also includes `dicom-transcode`, 84 | which lets you transcode DICOM files to other transfer syntaxes. 85 | 86 | ### Development tools 87 | 88 | - [`dictionary-builder`](dictionary-builder) is an independent application that 89 | generates code and other data structures for a DICOM standard dictionary. 90 | 91 | ## Building 92 | 93 | You can use Cargo to build all crates in the repository. 94 | 95 | ```sh 96 | cargo build 97 | ``` 98 | 99 | Other than the parts needed to build a pure Rust project, 100 | no other development dependencies are necessary 101 | unless certain extensions are included via Cargo features. 102 | Consult each crate for guidelines on selecting features to suit your needs. 103 | 104 | Minimum supported Rust version is 1.72.0 and only applies to the library crates with default features. 105 | Binary crates and extra features may require a newer version of Rust. 106 | 107 | ## Roadmap & Contributing 108 | 109 | This project is under active development. 110 | 111 | Your feedback during the development of these solutions is welcome. Please see the [wiki](https://github.com/Enet4/dicom-rs/wiki) 112 | for additional guidelines related to the project's roadmap. 113 | See also the [contributor guidelines](CONTRIBUTING.md) and the project's [Code of Conduct](CODE_OF_CONDUCT.md). 114 | 115 | ## License 116 | 117 | Licensed under either of 118 | 119 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 120 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 121 | 122 | at your option. 123 | 124 | Unless you explicitly state otherwise, any contribution intentionally submitted 125 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 126 | additional terms or conditions. 127 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv="1.72.0" -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .vscode/ 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-core" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | description = "Efficient and practical core library for DICOM compliant systems" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | keywords = ["dicom"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | chrono = { version = "0.4.31", default-features = false, features = ["std", "clock"] } 15 | itertools = "0.14" 16 | num-traits = "0.2.12" 17 | safe-transmute = "0.11.0" 18 | smallvec = "1.6.1" 19 | snafu = "0.8" 20 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `core` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-core.svg)](https://crates.io/crates/dicom-core) 4 | [![Documentation](https://docs.rs/dicom-core/badge.svg)](https://docs.rs/dicom-core) 5 | 6 | This sub-project implements the essential data structures and mechanisms 7 | for dealing with DICOM information and communication formats, 8 | thus serving as a center piece for other crates in [DICOM-rs]. 9 | 10 | This crate is part of the [DICOM-rs] project 11 | and is contained by the parent crate [`dicom`](https://crates.io/crates/dicom). 12 | 13 | [DICOM-rs]: https://github.com/Enet4/dicom-rs 14 | -------------------------------------------------------------------------------- /core/src/dictionary/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the concept of a DICOM data dictionary. 2 | //! 3 | //! The standard data dictionary is available in the [`dicom-dictionary-std`] crate. 4 | 5 | mod data_element; 6 | pub mod stub; 7 | mod uid; 8 | 9 | pub use data_element::{ 10 | DataDictionary, DataDictionaryEntry, DataDictionaryEntryBuf, DataDictionaryEntryRef, TagByName, 11 | TagRange, VirtualVr, 12 | }; 13 | 14 | pub use uid::{UidDictionary, UidDictionaryEntry, UidDictionaryEntryRef, UidType}; 15 | -------------------------------------------------------------------------------- /core/src/dictionary/stub.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a stub dictionary. 2 | 3 | use super::{DataDictionary, DataDictionaryEntryRef}; 4 | use crate::header::Tag; 5 | 6 | /// An empty attribute dictionary. 7 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 8 | pub struct StubDataDictionary; 9 | 10 | impl DataDictionary for StubDataDictionary { 11 | type Entry = DataDictionaryEntryRef<'static>; 12 | fn by_name(&self, _: &str) -> Option<&DataDictionaryEntryRef<'static>> { 13 | None 14 | } 15 | 16 | fn by_tag(&self, _: Tag) -> Option<&DataDictionaryEntryRef<'static>> { 17 | None 18 | } 19 | } 20 | 21 | impl DataDictionary for &'_ StubDataDictionary { 22 | type Entry = DataDictionaryEntryRef<'static>; 23 | fn by_name(&self, _: &str) -> Option<&DataDictionaryEntryRef<'static>> { 24 | None 25 | } 26 | 27 | fn by_tag(&self, _: Tag) -> Option<&DataDictionaryEntryRef<'static>> { 28 | None 29 | } 30 | } 31 | 32 | impl DataDictionary for Box { 33 | type Entry = DataDictionaryEntryRef<'static>; 34 | fn by_name(&self, _: &str) -> Option<&DataDictionaryEntryRef<'static>> { 35 | None 36 | } 37 | 38 | fn by_tag(&self, _: Tag) -> Option<&DataDictionaryEntryRef<'static>> { 39 | None 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/dictionary/uid.rs: -------------------------------------------------------------------------------- 1 | //! Core UID dictionary types 2 | 3 | use std::str::FromStr; 4 | 5 | /// Type trait for a dictionary of known DICOM unique identifiers (UIDs). 6 | /// 7 | /// UID dictionaries provide the means to 8 | /// look up information at run-time about a certain UID. 9 | /// 10 | /// The methods herein have no generic parameters, 11 | /// so as to enable being used as a trait object. 12 | pub trait UidDictionary { 13 | /// The type of the dictionary entry. 14 | type Entry: UidDictionaryEntry; 15 | 16 | /// Fetch an entry by its usual keyword (e.g. CTImageStorage). 17 | /// Aliases (or keywords) 18 | /// are usually in UpperCamelCase, 19 | /// not separated by spaces, 20 | /// and are case sensitive. 21 | fn by_keyword(&self, keyword: &str) -> Option<&Self::Entry>; 22 | 23 | /// Fetch an entry by its UID. 24 | fn by_uid(&self, uid: &str) -> Option<&Self::Entry>; 25 | } 26 | 27 | /// UID dictionary entry type 28 | pub trait UidDictionaryEntry { 29 | /// Get the UID proper. 30 | fn uid(&self) -> &str; 31 | 32 | /// Get the full name of the identifier. 33 | fn name(&self) -> &str; 34 | 35 | /// The alias of the UID, with no spaces, usually in UpperCamelCase. 36 | fn alias(&self) -> &str; 37 | 38 | /// Get whether the UID is retired. 39 | fn is_retired(&self) -> bool; 40 | } 41 | 42 | /// A data type for a dictionary entry using string slices 43 | /// for its data. 44 | #[derive(Debug, PartialEq, Clone)] 45 | pub struct UidDictionaryEntryRef<'a> { 46 | /// The UID proper 47 | pub uid: &'a str, 48 | /// The full name of the identifier, 49 | /// which may contain spaces 50 | pub name: &'a str, 51 | /// The alias of the identifier, 52 | /// with no spaces, usually in UpperCamelCase 53 | pub alias: &'a str, 54 | /// The type of UID 55 | pub r#type: UidType, 56 | /// Whether this SOP class is retired 57 | pub retired: bool, 58 | } 59 | 60 | impl<'a> UidDictionaryEntryRef<'a> { 61 | pub const fn new( 62 | uid: &'a str, 63 | name: &'a str, 64 | alias: &'a str, 65 | r#type: UidType, 66 | retired: bool, 67 | ) -> Self { 68 | UidDictionaryEntryRef { 69 | uid, 70 | name, 71 | alias, 72 | r#type, 73 | retired, 74 | } 75 | } 76 | } 77 | 78 | impl UidDictionaryEntry for UidDictionaryEntryRef<'_> { 79 | fn uid(&self) -> &str { 80 | self.uid 81 | } 82 | 83 | fn name(&self) -> &str { 84 | self.name 85 | } 86 | 87 | fn alias(&self) -> &str { 88 | self.alias 89 | } 90 | 91 | fn is_retired(&self) -> bool { 92 | self.retired 93 | } 94 | } 95 | 96 | /// Enum for all UID types recognized by the standard. 97 | #[non_exhaustive] 98 | #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] 99 | pub enum UidType { 100 | /// SOP Class 101 | SopClass, 102 | /// Meta SOP Class 103 | MetaSopClass, 104 | /// Transfer Syntax 105 | TransferSyntax, 106 | /// Well-known SOP Instance 107 | WellKnownSopInstance, 108 | /// DICOM UIDs as a Coding Scheme 109 | DicomUidsAsCodingScheme, 110 | /// Coding Scheme 111 | CodingScheme, 112 | /// Application Context Name 113 | ApplicationContextName, 114 | /// Service Class 115 | ServiceClass, 116 | /// Application Hosting Model 117 | ApplicationHostingModel, 118 | /// Mapping Resource 119 | MappingResource, 120 | /// LDAP OID 121 | LdapOid, 122 | /// Synchronization Frame of Reference 123 | SynchronizationFrameOfReference, 124 | } 125 | 126 | impl FromStr for UidType { 127 | type Err = (); 128 | 129 | fn from_str(s: &str) -> Result { 130 | match s.trim() { 131 | "SOP Class" => Ok(UidType::SopClass), 132 | "Meta SOP Class" => Ok(UidType::MetaSopClass), 133 | "Transfer Syntax" => Ok(UidType::TransferSyntax), 134 | "Well-known SOP Instance" => Ok(UidType::WellKnownSopInstance), 135 | "DICOM UIDs as a Coding Scheme" => Ok(UidType::DicomUidsAsCodingScheme), 136 | "Coding Scheme" => Ok(UidType::CodingScheme), 137 | "Application Context Name" => Ok(UidType::ApplicationContextName), 138 | "Service Class" => Ok(UidType::ServiceClass), 139 | "Application Hosting Model" => Ok(UidType::ApplicationHostingModel), 140 | "Mapping Resource" => Ok(UidType::MappingResource), 141 | "LDAP OID" => Ok(UidType::LdapOid), 142 | "Synchronization Frame of Reference" => Ok(UidType::SynchronizationFrameOfReference), 143 | _ => Err(()), 144 | } 145 | } 146 | } 147 | 148 | impl std::fmt::Display for UidType { 149 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 150 | let str = match self { 151 | UidType::SopClass => "SOP Class", 152 | UidType::MetaSopClass => "Meta SOP Class", 153 | UidType::TransferSyntax => "Transfer Syntax", 154 | UidType::WellKnownSopInstance => "Well-known SOP Instance", 155 | UidType::DicomUidsAsCodingScheme => "DICOM UIDs as a Coding Scheme", 156 | UidType::CodingScheme => "Coding Scheme", 157 | UidType::ApplicationContextName => "Application Context Name", 158 | UidType::ServiceClass => "Service Class", 159 | UidType::ApplicationHostingModel => "Application Hosting Model", 160 | UidType::MappingResource => "Mapping Resource", 161 | UidType::LdapOid => "LDAP OID", 162 | UidType::SynchronizationFrameOfReference => "Synchronization Frame of Reference", 163 | }; 164 | f.write_str(str) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /core/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Prelude module. 2 | //! 3 | //! You may import all symbols within for convenient usage of this library. 4 | //! 5 | //! # Example 6 | //! 7 | //! ```ignore 8 | //! use dicom_core::prelude::*; 9 | //! ``` 10 | 11 | pub use crate::value::{AsRange as _, DicomDate, DicomDateTime, DicomTime}; 12 | pub use crate::{dicom_value, DataElement, DicomValue, Tag, VR}; 13 | pub use crate::{header::HasLength as _, DataDictionary as _}; 14 | -------------------------------------------------------------------------------- /core/src/value/serialize.rs: -------------------------------------------------------------------------------- 1 | //! Encoding of primitive values. 2 | use crate::value::{DicomDate, DicomDateTime, DicomTime}; 3 | use std::io::{Result as IoResult, Write}; 4 | 5 | /** Encode a single date in accordance to the DICOM Date (DA) 6 | * value representation. 7 | */ 8 | pub fn encode_date(mut to: W, date: DicomDate) -> IoResult 9 | where 10 | W: Write, 11 | { 12 | // YYYY(MM(DD)?)? 13 | let len = date.to_encoded().len(); 14 | write!(to, "{}", date.to_encoded())?; 15 | Ok(len) 16 | } 17 | 18 | /** Encode a single time value in accordance to the DICOM Time (TM) 19 | * value representation. 20 | */ 21 | pub fn encode_time(mut to: W, time: DicomTime) -> IoResult 22 | where 23 | W: Write, 24 | { 25 | // HH(MM(SS(.F{1,6})?)?)? 26 | let len = time.to_encoded().len(); 27 | write!(to, "{}", time.to_encoded())?; 28 | Ok(len) 29 | } 30 | 31 | /** Encode a single date-time value in accordance to the DICOM DateTime (DT) 32 | * value representation. 33 | */ 34 | pub fn encode_datetime(mut to: W, dt: DicomDateTime) -> IoResult 35 | where 36 | W: Write, 37 | { 38 | let value = dt.to_encoded(); 39 | let len = value.len(); 40 | write!(to, "{}", value)?; 41 | Ok(len) 42 | } 43 | 44 | #[cfg(test)] 45 | mod test { 46 | use super::*; 47 | use chrono::FixedOffset; 48 | use std::str::from_utf8; 49 | 50 | #[test] 51 | fn test_encode_date() { 52 | let mut data = vec![]; 53 | encode_date(&mut data, DicomDate::from_ym(1985, 12).unwrap()).unwrap(); 54 | assert_eq!(&data, &*b"198512"); 55 | } 56 | 57 | #[test] 58 | fn test_encode_time() { 59 | let mut data = vec![]; 60 | encode_time( 61 | &mut data, 62 | DicomTime::from_hms_micro(23, 59, 48, 123456).unwrap(), 63 | ) 64 | .unwrap(); 65 | assert_eq!(&data, &*b"235948.123456"); 66 | 67 | let mut data = vec![]; 68 | encode_time(&mut data, DicomTime::from_hms(12, 0, 30).unwrap()).unwrap(); 69 | assert_eq!(&data, &*b"120030"); 70 | 71 | let mut data = vec![]; 72 | encode_time(&mut data, DicomTime::from_h(9).unwrap()).unwrap(); 73 | assert_eq!(&data, &*b"09"); 74 | } 75 | 76 | #[test] 77 | fn test_encode_datetime() { 78 | let mut data = vec![]; 79 | let bytes = encode_datetime( 80 | &mut data, 81 | DicomDateTime::from_date_and_time( 82 | DicomDate::from_ymd(1985, 12, 31).unwrap(), 83 | DicomTime::from_hms_micro(23, 59, 48, 123_456).unwrap(), 84 | ) 85 | .unwrap(), 86 | ) 87 | .unwrap(); 88 | assert_eq!(from_utf8(&data).unwrap(), "19851231235948.123456"); 89 | assert_eq!(bytes, 21); 90 | 91 | let mut data = vec![]; 92 | let offset = FixedOffset::east_opt(3600).unwrap(); 93 | let bytes = encode_datetime( 94 | &mut data, 95 | DicomDateTime::from_date_and_time_with_time_zone( 96 | DicomDate::from_ymd(2018, 12, 24).unwrap(), 97 | DicomTime::from_h(4).unwrap(), 98 | offset, 99 | ) 100 | .unwrap(), 101 | ) 102 | .unwrap(); 103 | assert_eq!(from_utf8(&data).unwrap(), "2018122404+0100"); 104 | assert_eq!(bytes, 15); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /core/tests/dicom_value.rs: -------------------------------------------------------------------------------- 1 | //! Separate test suite for using `dicom_value!` in an isolated context, 2 | //! without direct access to dependency `smallvec` 3 | 4 | // empty module makes `smallvec` dependency unreachable, 5 | // as would be typical in dependents of `dicom_core` 6 | // unless they include it themselves 7 | mod smallvec {} 8 | 9 | #[test] 10 | fn use_dicom_value() { 11 | use dicom_core::dicom_value; 12 | 13 | // multiple string literals with variant, no trailing comma 14 | let value = dicom_value!(Strs, ["BASE", "LIGHT", "DARK"]); 15 | assert_eq!( 16 | value.to_multi_str().as_ref(), 17 | &["BASE".to_owned(), "LIGHT".to_owned(), "DARK".to_owned(),], 18 | ); 19 | 20 | // single string with variant 21 | let value = dicom_value!(Str, "PALETTE COLOR "); 22 | assert_eq!(value.to_string(), "PALETTE COLOR",); 23 | 24 | // numeric values 25 | let value = dicom_value!(U16, [1, 2, 5]); 26 | assert_eq!(value.to_multi_int::().unwrap(), &[1, 2, 5],); 27 | } 28 | -------------------------------------------------------------------------------- /core/tests/using_prelude.rs: -------------------------------------------------------------------------------- 1 | use dicom_core::prelude::*; 2 | 3 | #[test] 4 | fn can_use_prelude() { 5 | // can refer to `DataElement`, `Tag`, `VR`, and `dicom_value!` 6 | let elem: DataElement = 7 | DataElement::new( 8 | Tag(0x0010, 0x0010), 9 | VR::PN, 10 | dicom_value!(Str, "Simões^João"), 11 | ); 12 | let length = elem.length().0; 13 | assert_eq!(length as usize, "Simões^João".len()); 14 | 15 | // can call `by_tag` 16 | assert_eq!( 17 | dicom_core::dictionary::stub::StubDataDictionary.by_tag(Tag(0x0010, 0x0010)), 18 | None, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /devtools/dictionary-builder/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .vscode/ 3 | Cargo.lock 4 | /tags.rs 5 | -------------------------------------------------------------------------------- /devtools/dictionary-builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-dictionary-builder" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | description = "A generator of DICOM dictionaries from standard documentation and other sources" 6 | edition = "2018" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/Enet4/dicom-rs" 9 | categories = ["command-line-utilities"] 10 | keywords = ["dicom", "generator", "dictionary"] 11 | readme = "README.md" 12 | 13 | [[bin]] 14 | name = "dicom-dictionary-builder" 15 | path = "src/main.rs" 16 | 17 | [dependencies] 18 | clap = { version = "4.0.18", features = ["cargo", "derive"] } 19 | serde = { version = "1.0.55", features = ["derive"] } 20 | heck = "0.5.0" 21 | ureq = "3.0.11" 22 | sxd-document = "0.3.2" 23 | eyre = "0.6.12" 24 | sxd-xpath = "0.4.2" 25 | 26 | [dependencies.regex] 27 | version = "1.6.0" 28 | default-features = false 29 | features = ["std", "perf", "unicode-case", "unicode-perl"] 30 | -------------------------------------------------------------------------------- /devtools/dictionary-builder/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `dictionary-builder` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-dictionary-builder.svg)](https://crates.io/crates/dicom-dictionary-builder) 4 | [![Documentation](https://docs.rs/dicom-dictionary-builder/badge.svg)](https://docs.rs/dicom-dictionary-builder) 5 | 6 | This sub-project is a tool for generating machine readable attribute dictionaries from the DICOM standard. 7 | At the moment, the tool is capable of parsing .dic files from the DCMTK project. 8 | 9 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 10 | 11 | ## Building 12 | 13 | ```bash 14 | cargo build --release 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```text 20 | DICOM dictionary builder 21 | 22 | Usage: dicom-dictionary-builder 23 | 24 | Commands: 25 | data-element Fetch and build a dictionary of DICOM data elements (tags) 26 | uids Fetch and build a dictionary of DICOM unique identifiers 27 | help Print this message or the help of the given subcommand(s) 28 | 29 | Options: 30 | -h, --help Print help 31 | ``` 32 | 33 | After specifying which dictionary is intended, 34 | the next argument is usually its source, 35 | which can be either a file or a hyperlink. 36 | 37 | Fetching a data element (tags) dictionary: 38 | 39 | ```text 40 | Fetch and build a dictionary of DICOM data elements (tags) 41 | 42 | Usage: dicom-dictionary-builder data-element [OPTIONS] [FROM] 43 | 44 | Arguments: 45 | [FROM] Path or URL to the data element dictionary [default: https://raw.githubusercontent.com/DCMTK/dcmtk/master/dcmdata/data/dicom.dic] 46 | 47 | Options: 48 | -o The output file [default: tags.rs] 49 | --ignore-retired Ignore retired DICOM tags 50 | --deprecate-retired Mark retired DICOM tags as deprecated 51 | -h, --help Print help 52 | ``` 53 | 54 | Fetching a UID dictionary: 55 | 56 | ```text 57 | Usage: dicom-dictionary-builder uids [OPTIONS] [FROM] 58 | 59 | Arguments: 60 | [FROM] Path or URL to the XML file containing the UID values tables [default: https://dicom.nema.org/medical/dicom/current/source/docbook/part06/part06.xml] 61 | 62 | Options: 63 | -o The output file [default: uids.rs] 64 | --ignore-retired Ignore retired UIDs 65 | --deprecate-retired Mark retired UIDs as deprecated 66 | --feature-gate Whether to gate different UID types on Cargo features 67 | -h, --help Print help 68 | ``` 69 | 70 | **Note:** If retrieving part06.xml from the official DICOM server 71 | fails due to the TLS connection not initializing, 72 | try downloading the file with another software 73 | and passing the path to the file manually. 74 | -------------------------------------------------------------------------------- /devtools/dictionary-builder/src/common.rs: -------------------------------------------------------------------------------- 1 | /// How to process retired entries 2 | #[derive(Debug, Copy, Clone, PartialEq)] 3 | pub enum RetiredOptions { 4 | /// ignore retired attributes 5 | Ignore, 6 | /// include retired attributes 7 | Include { 8 | /// mark constants as deprecated 9 | deprecate: bool, 10 | }, 11 | } 12 | 13 | impl RetiredOptions { 14 | /// Create retired options from two flags. 15 | /// `ignore` takes precedence over `deprecate. 16 | pub fn from_flags(ignore: bool, deprecate: bool) -> Self { 17 | if ignore { 18 | RetiredOptions::Ignore 19 | } else { 20 | RetiredOptions::Include { deprecate } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /devtools/dictionary-builder/src/main.rs: -------------------------------------------------------------------------------- 1 | //! A simple application that downloads the data dictionary and creates code or 2 | //! data to reproduce it in the core library. 3 | //! 4 | //! ### How to use 5 | //! 6 | //! Run the application with one of the following subcommands: 7 | //! 8 | //! - **`data-element`** or **`tags`**: DICOM data element dictionary 9 | //! - **`uid`** or **`uids`**: DICOM unique identifiers dictionary 10 | //! 11 | //! It will automatically retrieve dictionary specifications 12 | //! from a credible source and output the result as a Rust code file 13 | //! or some other supported format. 14 | //! Future versions may enable different kinds of outputs and dictionaries. 15 | //! 16 | //! Please use the `--help` flag for the full usage information. 17 | 18 | use clap::{Parser, Subcommand}; 19 | 20 | mod common; 21 | mod tags; 22 | mod uids; 23 | 24 | /// DICOM dictionary builder 25 | #[derive(Debug, Parser)] 26 | struct App { 27 | #[clap(subcommand)] 28 | command: BuilderSubcommand, 29 | } 30 | 31 | #[derive(Debug, Subcommand)] 32 | enum BuilderSubcommand { 33 | #[clap(name("data-element"))] 34 | DataElement(tags::DataElementApp), 35 | #[clap(name("uids"))] 36 | Uid(uids::UidApp), 37 | } 38 | 39 | fn main() { 40 | match App::parse() { 41 | App { 42 | command: BuilderSubcommand::DataElement(app), 43 | } => tags::run(app), 44 | App { 45 | command: BuilderSubcommand::Uid(app), 46 | } => uids::run(app), 47 | } 48 | .unwrap() 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::App; 54 | use clap::CommandFactory; 55 | 56 | #[test] 57 | fn verify_cli() { 58 | App::command().debug_assert(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /dictionary-std/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-dictionary-std" 3 | version = "0.8.0" 4 | authors = ["Eduardo Pinho "] 5 | description = "Standard DICOM attribute dictionary" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | keywords = ["dicom", "dictionary"] 11 | readme = "README.md" 12 | 13 | [features] 14 | default = [] 15 | 16 | # run-time deployed DICOM dictionaries 17 | sop-class = [] 18 | meta-sop-class = [] 19 | transfer-syntax = [] 20 | well-known-sop-instance = [] 21 | dicom-uid-as-coding-scheme = [] 22 | coding-scheme = [] 23 | application-context-name = [] 24 | service-class = [] 25 | application-hosting-model = [] 26 | mapping-resource = [] 27 | ldap-oid = [] 28 | synchronization-frame-of-reference = [] 29 | 30 | [dependencies] 31 | dicom-core = { path = "../core", version = "0.8.1" } 32 | once_cell = "1.18.0" 33 | -------------------------------------------------------------------------------- /dictionary-std/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs standard dictionary 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-dictionary-std.svg)](https://crates.io/crates/dicom-dictionary-std) 4 | [![Documentation](https://docs.rs/dicom-dictionary-std/badge.svg)](https://docs.rs/dicom-dictionary-std) 5 | 6 | This sub-project uses entries generated by the 7 | [`dictionary_builder`](https://crates.io/crates/dictionary_builder) 8 | to provide the standard DICOM data dictionary. 9 | 10 | This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project 11 | and is contained by the parent crate [`dicom`](https://crates.io/crates/dicom). 12 | -------------------------------------------------------------------------------- /dictionary-std/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate implements standard DICOM dictionaries and constants. 2 | //! 3 | //! ## Run-time dictinaries 4 | //! 5 | //! The following modules provide definitions for dictionaries 6 | //! which can be queried during a program's lifetime: 7 | //! 8 | //! - [`data_element`]: Contains all information about the 9 | //! DICOM attributes specified in the standard, 10 | //! and it will be used by default in most other abstractions available. 11 | //! When not using private tags, this dictionary should suffice. 12 | //! - `sop_class` (requires Cargo feature **sop-class**): 13 | //! Contains information about DICOM Service-Object Pair (SOP) classes 14 | //! and their respective unique identifiers. 15 | //! 16 | //! The records in these dictionaries are typically collected 17 | //! from [DICOM PS3.6] directly, 18 | //! but they may be obtained through other sources. 19 | //! Each dictionary is provided as a singleton 20 | //! behind a unit type for efficiency and ease of use. 21 | //! 22 | //! [DICOM PS3.6]: https://dicom.nema.org/medical/dicom/current/output/chtml/part06/ps3.6.html 23 | //! 24 | //! ## Constants 25 | //! 26 | //! The following modules contain constant declarations, 27 | //! which perform an equivalent mapping at compile time, 28 | //! thus without incurring a look-up cost: 29 | //! 30 | //! - [`tags`], which map an attribute alias to a DICOM tag 31 | //! - [`uids`], for various normative DICOM unique identifiers 32 | pub mod data_element; 33 | 34 | #[cfg(feature = "sop-class")] 35 | pub mod sop_class; 36 | pub mod tags; 37 | pub mod uids; 38 | 39 | pub use data_element::{StandardDataDictionary, StandardDataDictionaryRegistry}; 40 | #[cfg(feature = "sop-class")] 41 | pub use sop_class::StandardSopClassDictionary; 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use dicom_core::Tag; 46 | 47 | /// tests for just a few attributes to make sure that the tag constants 48 | /// were well installed into the crate 49 | #[test] 50 | fn tags_constants_available() { 51 | use crate::tags::*; 52 | assert_eq!(PATIENT_NAME, Tag(0x0010, 0x0010)); 53 | assert_eq!(MODALITY, Tag(0x0008, 0x0060)); 54 | assert_eq!(PIXEL_DATA, Tag(0x7FE0, 0x0010)); 55 | assert_eq!(STATUS, Tag(0x0000, 0x0900)); 56 | } 57 | 58 | /// tests for the presence of a few UID constants 59 | #[test] 60 | fn uids_constants_available() { 61 | use crate::uids::*; 62 | assert_eq!(EXPLICIT_VR_LITTLE_ENDIAN, "1.2.840.10008.1.2.1"); 63 | assert_eq!(VERIFICATION, "1.2.840.10008.1.1"); 64 | assert_eq!(HOT_IRON_PALETTE, "1.2.840.10008.1.5.1"); 65 | assert_eq!( 66 | PATIENT_ROOT_QUERY_RETRIEVE_INFORMATION_MODEL_FIND, 67 | "1.2.840.10008.5.1.4.1.2.1.1" 68 | ); 69 | assert_eq!( 70 | STUDY_ROOT_QUERY_RETRIEVE_INFORMATION_MODEL_MOVE, 71 | "1.2.840.10008.5.1.4.1.2.2.2" 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /dictionary-std/src/sop_class.rs: -------------------------------------------------------------------------------- 1 | //! SOP class dictionary implementation 2 | 3 | use std::collections::HashMap; 4 | 5 | use dicom_core::dictionary::{UidDictionary, UidDictionaryEntryRef}; 6 | use once_cell::sync::Lazy; 7 | 8 | use crate::uids::SOP_CLASSES; 9 | 10 | static DICT: Lazy = Lazy::new(init_dictionary); 11 | 12 | /// Retrieve a singleton instance of the standard SOP class registry. 13 | /// 14 | /// Note that one does not generally have to call this 15 | /// unless when retrieving the underlying registry is important. 16 | /// The unit type [`StandardSopClassDictionary`] 17 | /// already provides a lazy loaded singleton implementing the necessary traits. 18 | #[inline] 19 | pub fn registry() -> &'static StandardUidRegistry { 20 | &DICT 21 | } 22 | 23 | /// Base data struct for a standard UID dictionary. 24 | #[derive(Debug)] 25 | pub struct StandardUidRegistry { 26 | /// mapping: keyword → entry 27 | by_keyword: HashMap<&'static str, &'static UidDictionaryEntryRef<'static>>, 28 | /// mapping: uid → entry 29 | by_uid: HashMap<&'static str, &'static UidDictionaryEntryRef<'static>>, 30 | } 31 | 32 | impl StandardUidRegistry { 33 | fn new() -> StandardUidRegistry { 34 | StandardUidRegistry { 35 | by_keyword: HashMap::new(), 36 | by_uid: HashMap::new(), 37 | } 38 | } 39 | 40 | /// record all of the given dictionary entries 41 | fn index_all(&mut self, entries: &'static [UidDictionaryEntryRef<'static>]) -> &mut Self { 42 | let entries_by_keyword = entries.iter().map(|e| (e.alias, e)); 43 | self.by_keyword.extend(entries_by_keyword); 44 | 45 | let entries_by_uid = entries.iter().map(|e| (e.uid, e)); 46 | self.by_uid.extend(entries_by_uid); 47 | 48 | self 49 | } 50 | } 51 | 52 | impl UidDictionary for StandardUidRegistry { 53 | type Entry = UidDictionaryEntryRef<'static>; 54 | 55 | #[inline] 56 | fn by_keyword(&self, keyword: &str) -> Option<&Self::Entry> { 57 | self.by_keyword.get(keyword).copied() 58 | } 59 | 60 | #[inline] 61 | fn by_uid(&self, uid: &str) -> Option<&Self::Entry> { 62 | self.by_uid.get(uid).copied() 63 | } 64 | } 65 | 66 | /// An SOP class dictionary which consults 67 | /// the library's global DICOM SOP class registry. 68 | /// 69 | /// This is the type which would generally be used 70 | /// whenever a program needs to translate an SOP class UID 71 | /// to its name or from its keyword (alias) back to a UID 72 | /// during a program's execution. 73 | /// Note that the [`uids`](crate::uids) module 74 | /// already provides easy to use constants for SOP classes. 75 | /// 76 | /// The dictionary index is automatically initialized upon the first use. 77 | #[derive(Debug, Default, Copy, Clone, Eq, Hash, PartialEq)] 78 | pub struct StandardSopClassDictionary; 79 | 80 | impl UidDictionary for StandardSopClassDictionary { 81 | type Entry = UidDictionaryEntryRef<'static>; 82 | 83 | #[inline] 84 | fn by_keyword(&self, keyword: &str) -> Option<&Self::Entry> { 85 | DICT.by_keyword(keyword) 86 | } 87 | 88 | #[inline] 89 | fn by_uid(&self, uid: &str) -> Option<&Self::Entry> { 90 | DICT.by_uid(uid) 91 | } 92 | } 93 | 94 | fn init_dictionary() -> StandardUidRegistry { 95 | let mut d = StandardUidRegistry::new(); 96 | 97 | // only index SOP classes in this one 98 | d.index_all(SOP_CLASSES); 99 | d 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use crate::StandardSopClassDictionary; 105 | use dicom_core::dictionary::{UidDictionary, UidDictionaryEntryRef, UidType}; 106 | 107 | // tests for just a few SOP classes to make sure that the entries 108 | // were well installed into the dictionary index 109 | #[test] 110 | fn can_fetch_sop_classes() { 111 | let dict = StandardSopClassDictionary::default(); 112 | 113 | let entry = dict.by_uid("1.2.840.10008.1.1"); 114 | assert_eq!( 115 | entry, 116 | Some(&UidDictionaryEntryRef { 117 | uid: "1.2.840.10008.1.1", 118 | alias: "Verification", 119 | name: "Verification SOP Class", 120 | retired: false, 121 | r#type: UidType::SopClass, 122 | }) 123 | ); 124 | 125 | let entry = dict.by_keyword("ComputedRadiographyImageStorage"); 126 | assert_eq!( 127 | entry, 128 | Some(&UidDictionaryEntryRef { 129 | uid: crate::uids::COMPUTED_RADIOGRAPHY_IMAGE_STORAGE, 130 | alias: "ComputedRadiographyImageStorage", 131 | name: "Computed Radiography Image Storage", 132 | retired: false, 133 | r#type: UidType::SopClass, 134 | }) 135 | ); 136 | 137 | let entry = dict.by_uid("1.2.840.10008.5.1.4.1.1.3"); 138 | assert_eq!( 139 | entry, 140 | Some(&UidDictionaryEntryRef { 141 | uid: "1.2.840.10008.5.1.4.1.1.3", 142 | alias: "UltrasoundMultiFrameImageStorageRetired", 143 | name: "Ultrasound Multi-frame Image Storage (Retired)", 144 | retired: true, 145 | r#type: UidType::SopClass, 146 | }) 147 | ); 148 | 149 | // no transfer syntaxes, only SOP classes 150 | let entry = dict.by_uid(crate::uids::EXPLICIT_VR_LITTLE_ENDIAN); 151 | assert_eq!(entry, None); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /dump/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-dump" 3 | version = "0.8.0" 4 | authors = ["Eduardo Pinho "] 5 | description = "A CLI tool for inspecting DICOM files" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | categories = ["command-line-utilities"] 11 | keywords = ["cli", "dicom", "dump"] 12 | readme = "README.md" 13 | 14 | [lib] 15 | name = "dicom_dump" 16 | path = "src/lib.rs" 17 | 18 | [[bin]] 19 | name = "dicom-dump" 20 | path = "src/main.rs" 21 | required-features = ["cli"] 22 | 23 | [features] 24 | default = ["cli", "sop-class"] 25 | sop-class = ["dicom-dictionary-std/sop-class"] 26 | cli = ["clap", "dicom-transfer-syntax-registry/inventory-registry"] 27 | 28 | [dependencies] 29 | snafu = "0.8" 30 | clap = { version = "4.0.18", features = ["derive"], optional = true } 31 | dicom-core = { path = "../core", version = "0.8.1" } 32 | dicom-encoding = { path = "../encoding", version = "0.8" } 33 | dicom-json = { version = "0.8.1", path = "../json" } 34 | dicom-object = { path = "../object/", version = "0.8.1" } 35 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.8.1", default-features = false } 36 | dicom-dictionary-std = { path = "../dictionary-std/", version = "0.8.0" } 37 | owo-colors = { version = "4.0.0-rc.1", features = ["supports-colors"] } 38 | serde_json = "1.0.108" 39 | terminal_size = "0.4.0" 40 | -------------------------------------------------------------------------------- /dump/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `dump` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-dump.svg)](https://crates.io/crates/dicom-dump) 4 | [![Documentation](https://docs.rs/dicom-dump/badge.svg)](https://docs.rs/dicom-dump) 5 | 6 | A command line utility for inspecting the contents of DICOM files 7 | by printing them in a human readable format. 8 | 9 | A programmatic API for dumping DICOM objects is also available. 10 | If you intend to use `dicom-dump` exclusively as a library, 11 | you can disable the `cli` Cargo feature. 12 | 13 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 14 | 15 | ## Usage 16 | 17 | ```none 18 | dicom-dump [FLAGS] [OPTIONS] ... 19 | 20 | FLAGS: 21 | --fail-first fail if any errors are encountered 22 | -h, --help Prints help information 23 | --no-text-limit whether text value width limit is disabled (limited to `width` by default) 24 | -V, --version Prints version information 25 | 26 | OPTIONS: 27 | --color color mode [default: auto] 28 | -w, --width the width of the display (default is to check automatically) 29 | 30 | ARGS: 31 | ... The DICOM file(s) to read 32 | ``` 33 | -------------------------------------------------------------------------------- /dump/src/main.rs: -------------------------------------------------------------------------------- 1 | //! A CLI tool for inspecting the contents of a DICOM file 2 | //! by printing it in a human readable format. 3 | use clap::Parser; 4 | use dicom_core::Tag; 5 | use dicom_dictionary_std::tags; 6 | use dicom_dump::{ColorMode, DumpFormat, DumpOptions}; 7 | use dicom_object::{file::OddLengthStrategy, OpenFileOptions, StandardDataDictionary}; 8 | use snafu::{Report, Whatever}; 9 | use std::io::{ErrorKind, IsTerminal}; 10 | use std::path::PathBuf; 11 | 12 | /// Exit code for when an error emerged while reading the DICOM file. 13 | const ERROR_READ: i32 = -2; 14 | /// Exit code for when an error emerged while dumping the file. 15 | const ERROR_PRINT: i32 = -3; 16 | 17 | /// Dump the contents of DICOM files 18 | #[derive(Debug, Parser)] 19 | #[command(version)] 20 | struct App { 21 | /// The DICOM file(s) to read 22 | #[clap(required = true)] 23 | files: Vec, 24 | /// Read the file up to this tag 25 | #[clap(long = "until", value_parser = parse_tag)] 26 | read_until: Option, 27 | /// Strategy for handling odd-length text values 28 | /// 29 | /// accept: Accept elements with an odd length as is, 30 | /// continuing data set reading normally. 31 | /// 32 | /// next_even: Assume that the real length is `length + 1`, 33 | /// as in the next even number. 34 | /// 35 | /// fail: Raise an error instead 36 | #[clap(short = 'o', long = "odd-length-strategy", value_parser = parse_strategy, default_value = "accept")] 37 | odd_length_strategy: OddLengthStrategy, 38 | /// Print text values to the end 39 | /// (limited to `width` by default). 40 | /// 41 | /// Does not apply if output is not a tty 42 | /// or if output type is json 43 | #[clap(long = "no-text-limit")] 44 | no_text_limit: bool, 45 | /// Print all values to the end 46 | /// (implies `no_text_limit`, limited to `width` by default) 47 | #[clap(long = "no-limit")] 48 | no_limit: bool, 49 | /// The width of the display 50 | /// (default is to check automatically). 51 | /// 52 | /// Does not apply if output is not a tty 53 | /// or if output type is json 54 | #[clap(short = 'w', long = "width")] 55 | width: Option, 56 | /// The color mode 57 | #[clap(long = "color", default_value = "auto")] 58 | color: ColorMode, 59 | /// Fail if any errors are encountered 60 | #[clap(long = "fail-first")] 61 | fail_first: bool, 62 | /// Output format 63 | #[arg(value_enum)] 64 | #[clap(short = 'f', long = "format", default_value = "text")] 65 | format: DumpFormat, 66 | } 67 | 68 | fn parse_strategy(s: &str) -> Result { 69 | match s { 70 | "accept" => Ok(OddLengthStrategy::Accept), 71 | "next_even" => Ok(OddLengthStrategy::NextEven), 72 | "fail" => Ok(OddLengthStrategy::Fail), 73 | _ => Err("invalid strategy"), 74 | } 75 | } 76 | 77 | fn parse_tag(s: &str) -> Result { 78 | use dicom_core::dictionary::DataDictionary as _; 79 | StandardDataDictionary.parse_tag(s).ok_or("invalid tag") 80 | } 81 | 82 | fn is_terminal() -> bool { 83 | std::io::stdout().is_terminal() 84 | } 85 | 86 | fn main() { 87 | run().unwrap_or_else(|e| { 88 | eprintln!("{}", Report::from_error(e)); 89 | std::process::exit(-2); 90 | }); 91 | } 92 | 93 | fn run() -> Result<(), Whatever> { 94 | let App { 95 | files: filenames, 96 | read_until, 97 | odd_length_strategy, 98 | no_text_limit, 99 | no_limit, 100 | width, 101 | color, 102 | fail_first, 103 | format, 104 | } = App::parse(); 105 | 106 | let width = width 107 | .or_else(|| terminal_size::terminal_size().map(|(width, _)| width.0 as u32)) 108 | .unwrap_or(120); 109 | 110 | let mut options = DumpOptions::new(); 111 | options 112 | .no_text_limit(no_text_limit) 113 | // No limit when output is not a terminal 114 | .no_limit(if !is_terminal() { true } else { no_limit }) 115 | .width(width) 116 | .color_mode(color) 117 | .format(format); 118 | let fail_first = filenames.len() == 1 || fail_first; 119 | let mut errors: i32 = 0; 120 | 121 | for filename in &filenames { 122 | // Write filename to stderr to make piping easier, i.e. dicom-dump -o json file.dcm | jq 123 | eprintln!("{}: ", filename.display()); 124 | 125 | let open_options = match read_until { 126 | Some(stop_tag) => OpenFileOptions::new().read_until(stop_tag), 127 | None => OpenFileOptions::new(), 128 | }; 129 | 130 | let open_options = open_options.odd_length_strategy(odd_length_strategy); 131 | 132 | match open_options.open_file(filename) { 133 | Err(e) => { 134 | eprintln!("{}", Report::from_error(e)); 135 | if fail_first { 136 | std::process::exit(ERROR_READ); 137 | } 138 | errors += 1; 139 | } 140 | Ok(mut obj) => { 141 | if options.format == DumpFormat::Json { 142 | // JSON output doesn't currently support encapsulated pixel data 143 | if let Ok(elem) = obj.element(tags::PIXEL_DATA) { 144 | if let dicom_core::value::Value::PixelSequence(_) = elem.value() { 145 | eprintln!("[WARN] Encapsulated pixel data not supported in JSON output, skipping"); 146 | obj.remove_element(tags::PIXEL_DATA); 147 | } 148 | } 149 | } 150 | if let Err(ref e) = options.dump_file(&obj) { 151 | if e.kind() == ErrorKind::BrokenPipe { 152 | // handle broken pipe separately with a no-op 153 | } else { 154 | eprintln!("[ERROR] {}", Report::from_error(e)); 155 | if fail_first { 156 | std::process::exit(ERROR_PRINT); 157 | } 158 | } 159 | errors += 1; 160 | } // else all good 161 | } 162 | }; 163 | } 164 | 165 | std::process::exit(errors); 166 | } 167 | 168 | #[cfg(test)] 169 | mod tests { 170 | use crate::App; 171 | use clap::CommandFactory; 172 | 173 | #[test] 174 | fn verify_cli() { 175 | App::command().debug_assert(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /echoscu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-echoscu" 3 | version = "0.8.0" 4 | authors = ["Eduardo Pinho "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/Enet4/dicom-rs" 8 | description = "A DICOM C-ECHO command line interface" 9 | categories = ["command-line-utilities"] 10 | keywords = ["dicom"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | clap = { version = "4.0.18", features = ["derive"] } 15 | dicom-core = { path = "../core", version = "0.8.1" } 16 | dicom-dictionary-std = { path = "../dictionary-std/", version = "0.8.0" } 17 | dicom-dump = { version = "0.8.0", path = "../dump", default-features = false } 18 | dicom-object = { path = "../object/", version = "0.8.1" } 19 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.8.1", default-features = false } 20 | dicom-ul = { path = "../ul", version = "0.8.1" } 21 | snafu = "0.8" 22 | tracing = "0.1.34" 23 | tracing-subscriber = "0.3.11" 24 | -------------------------------------------------------------------------------- /echoscu/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `echoscu` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-echoscu.svg)](https://crates.io/crates/dicom-echoscu) 4 | [![Documentation](https://docs.rs/dicom-echoscu/badge.svg)](https://docs.rs/dicom-echoscu) 5 | 6 | This is an implementation of the DICOM Verification C-ECHO SCU, 7 | which can be used for verifying DICOM nodes. 8 | 9 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 10 | 11 | ## Usage 12 | 13 | Note that this tool is not necessarily a drop-in replacement 14 | for `echoscu` tools in other DICOM software projects. 15 | 16 | ```none 17 | DICOM C-ECHO SCU 18 | 19 | USAGE: 20 | dicom-echoscu [FLAGS] [OPTIONS] 21 | 22 | FLAGS: 23 | -h, --help Prints help information 24 | -V, --version Prints version information 25 | -v, --verbose verbose mode 26 | 27 | OPTIONS: 28 | --called-ae-title 29 | the called Application Entity title, overrides AE title in address if present [default: ANY-SCP] 30 | 31 | --calling-ae-title the calling AE title [default: ECHOSCU] 32 | -m, --message-id the C-ECHO message ID [default: 1] 33 | 34 | ARGS: 35 | socket address to SCP, optionally with AE title (example: "QUERY-SCP@127.0.0.1:1045") 36 | ``` 37 | 38 | Example: 39 | 40 | ```sh 41 | dicom-echoscu --verbose MAIN-STORAGE@192.168.1.99:104 42 | ``` 43 | -------------------------------------------------------------------------------- /echoscu/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use dicom_core::{dicom_value, DataElement, VR}; 3 | use dicom_dictionary_std::{tags, uids}; 4 | use dicom_object::{mem::InMemDicomObject, StandardDataDictionary}; 5 | use dicom_ul::{ 6 | association::client::ClientAssociationOptions, 7 | pdu::{self, PDataValueType, Pdu}, 8 | }; 9 | use pdu::PDataValue; 10 | use snafu::{prelude::*, Whatever}; 11 | use tracing::{debug, error, info, warn, Level}; 12 | 13 | /// DICOM C-ECHO SCU 14 | #[derive(Debug, Parser)] 15 | #[command(version)] 16 | struct App { 17 | /// socket address to SCP, 18 | /// optionally with AE title 19 | /// (example: "QUERY-SCP@127.0.0.1:1045") 20 | addr: String, 21 | /// verbose mode 22 | #[arg(short = 'v', long = "verbose")] 23 | verbose: bool, 24 | /// the C-ECHO message ID 25 | #[arg(short = 'm', long = "message-id", default_value = "1")] 26 | message_id: u16, 27 | /// the calling AE title 28 | #[arg(long = "calling-ae-title", default_value = "ECHOSCU")] 29 | calling_ae_title: String, 30 | /// the called Application Entity title, 31 | /// overrides AE title in address if present [default: ANY-SCP] 32 | #[arg(long = "called-ae-title")] 33 | called_ae_title: Option, 34 | } 35 | 36 | fn main() { 37 | run().unwrap_or_else(|e| { 38 | error!("{}", snafu::Report::from_error(e)); 39 | std::process::exit(-2); 40 | }) 41 | } 42 | 43 | fn run() -> Result<(), Whatever> { 44 | let App { 45 | addr, 46 | verbose, 47 | message_id, 48 | called_ae_title, 49 | calling_ae_title, 50 | } = App::parse(); 51 | 52 | tracing::subscriber::set_global_default( 53 | tracing_subscriber::FmtSubscriber::builder() 54 | .with_max_level(if verbose { Level::DEBUG } else { Level::INFO }) 55 | .finish(), 56 | ) 57 | .whatever_context("Could not set up global logging subscriber") 58 | .unwrap_or_else(|e: Whatever| { 59 | eprintln!("[ERROR] {}", snafu::Report::from_error(e)); 60 | }); 61 | 62 | let mut association_opt = ClientAssociationOptions::new() 63 | .with_abstract_syntax("1.2.840.10008.1.1") 64 | .calling_ae_title(calling_ae_title); 65 | if let Some(called_ae_title) = called_ae_title { 66 | association_opt = association_opt.called_ae_title(called_ae_title); 67 | } 68 | let mut association = association_opt 69 | .establish_with(&addr) 70 | .whatever_context("Could not establish association with SCP")?; 71 | 72 | let pc = association 73 | .presentation_contexts() 74 | .first() 75 | .whatever_context("No presentation context accepted")? 76 | .clone(); 77 | 78 | if verbose { 79 | debug!("Association with {} successful", addr); 80 | } 81 | 82 | // commands are always in implicit VR LE 83 | let ts = dicom_transfer_syntax_registry::entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(); 84 | 85 | let obj = create_echo_command(message_id); 86 | 87 | let mut data = Vec::new(); 88 | 89 | obj.write_dataset_with_ts(&mut data, &ts) 90 | .whatever_context("Failed to construct C-ECHO request")?; 91 | 92 | association 93 | .send(&Pdu::PData { 94 | data: vec![PDataValue { 95 | presentation_context_id: pc.id, 96 | value_type: PDataValueType::Command, 97 | is_last: true, 98 | data, 99 | }], 100 | }) 101 | .whatever_context("Failed to send C-ECHO request")?; 102 | 103 | if verbose { 104 | debug!( 105 | "Echo message sent (msg id {}), awaiting reply...", 106 | message_id 107 | ); 108 | } 109 | 110 | let pdu = association 111 | .receive() 112 | .whatever_context("Could not receive response from SCP")?; 113 | 114 | match pdu { 115 | Pdu::PData { data } => { 116 | let data_value = &data[0]; 117 | let v = &data_value.data; 118 | 119 | let obj = InMemDicomObject::read_dataset_with_ts(v.as_slice(), &ts) 120 | .whatever_context("Failed to read response dataset from SCP")?; 121 | if verbose { 122 | dicom_dump::dump_object(&obj) 123 | .whatever_context("Failed to output DICOM response")?; 124 | } 125 | 126 | // check status 127 | let status = obj 128 | .element(tags::STATUS) 129 | .whatever_context("Missing Status code in response")? 130 | .to_int::() 131 | .whatever_context("Status code in response is not a valid integer")?; 132 | if verbose { 133 | debug!("Status: {:04X}H", status); 134 | } 135 | match status { 136 | // Success 137 | 0 => { 138 | if verbose { 139 | info!("✓ C-ECHO successful"); 140 | } 141 | } 142 | // Warning 143 | 1 | 0x0107 | 0x0116 | 0xB000..=0xBFFF => { 144 | warn!("Possible issue in C-ECHO (status code {:04X}H)", status); 145 | } 146 | 0xFF00 | 0xFF01 => { 147 | warn!( 148 | "Possible issue in C-ECHO: status is pending (status code {:04X}H)", 149 | status 150 | ); 151 | } 152 | 0xFE00 => { 153 | warn!("Operation cancelled"); 154 | } 155 | _ => { 156 | error!("C-ECHO failed (status code {:04X}H)", status); 157 | } 158 | } 159 | 160 | // msg ID response, should be equal to sent msg ID 161 | let got_msg_id: u16 = obj 162 | .element(tags::MESSAGE_ID_BEING_RESPONDED_TO) 163 | .whatever_context("Could not retrieve Message ID from response")? 164 | .to_int() 165 | .whatever_context("Message ID is not a valid integer")?; 166 | 167 | if message_id != got_msg_id { 168 | whatever!("Message ID mismatch"); 169 | } 170 | } 171 | pdu => whatever!("Unexpected PDU {:?}", pdu), 172 | } 173 | 174 | Ok(()) 175 | } 176 | 177 | fn create_echo_command(message_id: u16) -> InMemDicomObject { 178 | InMemDicomObject::command_from_element_iter([ 179 | // service 180 | DataElement::new(tags::AFFECTED_SOP_CLASS_UID, VR::UI, uids::VERIFICATION), 181 | // command 182 | DataElement::new(tags::COMMAND_FIELD, VR::US, dicom_value!(U16, [0x0030])), 183 | // message ID 184 | DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [message_id])), 185 | // data set type 186 | DataElement::new( 187 | tags::COMMAND_DATA_SET_TYPE, 188 | VR::US, 189 | dicom_value!(U16, [0x0101]), 190 | ), 191 | ]) 192 | } 193 | 194 | #[cfg(test)] 195 | mod tests { 196 | use crate::App; 197 | use clap::CommandFactory; 198 | 199 | #[test] 200 | fn verify_cli() { 201 | App::command().debug_assert(); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /encoding/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-encoding" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | description = "DICOM encoding and decoding primitives" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | categories = ["encoding"] 11 | keywords = ["dicom"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | dicom-core = { path = "../core", version = "0.8.1" } 16 | dicom-dictionary-std = { path = "../dictionary-std", version = "0.8.0" } 17 | encoding = "0.2.33" 18 | byteordered = "0.6" 19 | inventory = { version = "0.3.2", optional = true } 20 | snafu = "0.8" 21 | 22 | [features] 23 | default = [] 24 | inventory-registry = ['inventory'] 25 | -------------------------------------------------------------------------------- /encoding/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `encoding` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-encoding.svg)](https://crates.io/crates/dicom-encoding) 4 | [![Documentation](https://docs.rs/dicom-encoding/badge.svg)](https://docs.rs/dicom-encoding) 5 | 6 | This sub-project provides DICOM data encoding and decoding primitives. 7 | 8 | This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project 9 | and is contained by the parent crate [`dicom`](https://crates.io/crates/dicom). 10 | -------------------------------------------------------------------------------- /encoding/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(trivial_numeric_casts, unsafe_code, unstable_features)] 2 | #![warn( 3 | missing_debug_implementations, 4 | unused_qualifications, 5 | unused_import_braces 6 | )] 7 | #![allow(clippy::derive_partial_eq_without_eq)] 8 | //! DICOM encoding and decoding primitives. 9 | //! 10 | //! This crate provides interfaces and data structures for reading and writing 11 | //! data in accordance to the DICOM standard. This crate also hosts the concept 12 | //! of [transfer syntax specifier], which can be used to produce DICOM encoders 13 | //! and decoders at run-time. 14 | //! 15 | //! For the time being, all APIs are based on synchronous I/O. 16 | //! 17 | //! [transfer syntax specifier]: ./transfer_syntax/index.html 18 | 19 | pub mod adapters; 20 | pub mod decode; 21 | pub mod encode; 22 | pub mod text; 23 | pub mod transfer_syntax; 24 | 25 | pub use adapters::NeverPixelAdapter; 26 | pub use byteordered::Endianness; 27 | pub use decode::Decode; 28 | pub use encode::Encode; 29 | pub use transfer_syntax::AdapterFreeTransferSyntax; 30 | pub use transfer_syntax::Codec; 31 | pub use transfer_syntax::DataRWAdapter; 32 | pub use transfer_syntax::NeverAdapter; 33 | pub use transfer_syntax::TransferSyntax; 34 | pub use transfer_syntax::TransferSyntaxIndex; 35 | 36 | // public dependency re-export 37 | pub use snafu; 38 | 39 | // public dependency re-export 40 | #[cfg(feature = "inventory-registry")] 41 | pub use inventory; 42 | -------------------------------------------------------------------------------- /findscu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-findscu" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/Enet4/dicom-rs" 8 | description = "A DICOM C-FIND command line interface" 9 | categories = ["command-line-utilities"] 10 | keywords = ["dicom", "query", "search"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | dicom-core = { path = '../core', version = "0.8.1" } 15 | dicom-ul = { path = '../ul', version = "0.8.1" } 16 | dicom-object = { path = '../object', version = "0.8.1" } 17 | dicom-encoding = { path = "../encoding/", version = "0.8.1" } 18 | dicom-dictionary-std = { path = "../dictionary-std/", version = "0.8.0" } 19 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.8.1" } 20 | dicom-dump = { path = "../dump", default-features = false, version = "0.8.0" } 21 | clap = { version = "4.0.18", features = ["derive"] } 22 | snafu = "0.8" 23 | tracing = "0.1.36" 24 | tracing-subscriber = "0.3.15" 25 | -------------------------------------------------------------------------------- /findscu/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `findscu` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-findscu.svg)](https://crates.io/crates/dicom-findscu) 4 | [![Documentation](https://docs.rs/dicom-findscu/badge.svg)](https://docs.rs/dicom-findscu) 5 | 6 | This is an implementation of the DICOM Find SCU (C-FIND), 7 | which can be used to search for study and patient records in a DICOM archive. 8 | 9 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 10 | 11 | ## Usage 12 | 13 | Note that this tool is not necessarily a drop-in replacement 14 | for `findscu` tools in other DICOM software toolkits. 15 | Run `dicom-findscu --help` for more details. 16 | 17 | Basic usage includes searching for a study or patient by a certain attribute. 18 | The following query/retrieve information models are supported at the moment: 19 | 20 | - **`-S`**: Study Root Query/Retrieve Information Model – FIND (default) 21 | - **`-P`**: Patient Root Query/Retrieve Information Model - FIND 22 | - **`-W`**: Modality Worklist Information Model – FIND 23 | 24 | There are three _non-exclusive_ ways to specify a DICOM query: 25 | 26 | ### Passing a DICOM query object file 27 | 28 | You may optionally provide a path to a DICOM query object file 29 | to bootstrap your query object, 30 | otherwise you start with an empty one. 31 | There are currently no tools in DICOM-rs 32 | to assist in the process of creating these objects, 33 | but one can convert DCMTK DICOM data dumps 34 | into compatible DICOM query objects, 35 | or write these tools yourself. 36 | 37 | ```sh 38 | # query is defined in query.dcm 39 | dicom-findscu PACS@pacs.example.com:1045 --study query.dcm 40 | ``` 41 | 42 | ### Passing a query text file 43 | 44 | An easier approach to specifying queries is 45 | through the command line argument `--query-file «file»`. 46 | The text file should contain a sequence of lines, 47 | each of the form `«field_path»=«field_value»`, where: 48 | 49 | - `field_path` is a data element selector path 50 | (see the element selector syntax below); 51 | - and `field_value` is the respective value or pattern to match 52 | against the value of the specified DICOM attribute. 53 | It can be empty, which in that case the `=` may also be left out. 54 | 55 | For example, given the file `query.txt`: 56 | 57 | ```none 58 | # comments are supported 59 | AccessionNumber 60 | ScheduledProcedureStepSequence.Modality=MR 61 | ScheduledProcedureStepSequence.ScheduledProcedureStepStartDate=20240703 62 | ``` 63 | 64 | You can do: 65 | 66 | ```sh 67 | dicom-findscu PACS@pacs.example.com:1045 -W --query-file query.txt 68 | ``` 69 | 70 | ### Using the multi-value `-q` option 71 | 72 | Finally, the `-q` option accepts multiple query values 73 | of the same form as in `--query-file`. 74 | See more examples below. 75 | 76 | Each of these forms will extend and override the query object in this order. 77 | 78 | #### Selector syntax 79 | 80 | Simple attribute selectors comprise a single data element key, 81 | specified by a standard DICOM tag 82 | (in one of the forms `(gggg,eeee)`, `gggg,eeee`, or `ggggeeee`) 83 | or a tag keyword name such as `PatientName`. 84 | To specify a sequence, use multiple of these separated by a dot 85 | (e.g. `ScheduledProcedureStepSequence.0040,0020`). 86 | Nested attributes will automatically construct intermediate sequences as needed. 87 | 88 | #### Examples 89 | 90 | ```sh 91 | # query application entity STORAGE for a study with the accession number A123 92 | dicom-findscu STORAGE@pacs.example.com:1045 --study -q AccessionNumber=A123 93 | 94 | # query application entity PACS for patients born in 1990-12-25 95 | dicom-findscu PACS@pacs.example.com:1045 --patient -q PatientBirthDate=19901225 96 | 97 | # wild-card query: grab a list of all study instance UIDs 98 | dicom-findscu PACS@pacs.example.com:1045 -S -q "StudyInstanceUID=*" 99 | 100 | # retrieve the modality worklist information 101 | # for scheduled procedures where the patient has arrived 102 | dicom-findscu INFO@pacs.example.com:1045 --mwl \ 103 | -q ScheduledProcedureStepSequence.ScheduledProcedureStepStatus=ARRIVED 104 | ``` 105 | -------------------------------------------------------------------------------- /findscu/src/query.rs: -------------------------------------------------------------------------------- 1 | //! Module for parsing query text pieces into DICOM queries. 2 | 3 | use std::str::FromStr; 4 | 5 | use dicom_core::ops::{ApplyOp, AttributeAction, AttributeOp, AttributeSelector}; 6 | use dicom_core::DataDictionary; 7 | use dicom_core::PrimitiveValue; 8 | use dicom_core::Tag; 9 | use dicom_core::VR; 10 | use dicom_dictionary_std::StandardDataDictionary; 11 | use dicom_object::InMemDicomObject; 12 | use snafu::whatever; 13 | use snafu::{OptionExt, ResultExt, Whatever}; 14 | 15 | #[derive(Debug, Clone, Eq, Hash, PartialEq)] 16 | struct TermQuery { 17 | selector: AttributeSelector, 18 | match_value: String, 19 | } 20 | 21 | /// Term queries can be parsed with the syntax `«tag»=«value»`, 22 | /// where `«tag»` is either a DICOM tag group-element pair 23 | /// or the respective tag keyword, 24 | /// and `=«value»` is optional. 25 | impl FromStr for TermQuery { 26 | type Err = Whatever; 27 | 28 | fn from_str(s: &str) -> Result { 29 | let mut parts = s.split('='); 30 | 31 | let selector_part = parts.next().whatever_context("empty query")?; 32 | let value_part = parts.next().unwrap_or_default(); 33 | 34 | let selector: AttributeSelector = StandardDataDictionary 35 | .parse_selector(selector_part) 36 | .whatever_context("could not resolve query field path")?; 37 | 38 | Ok(TermQuery { 39 | selector, 40 | match_value: value_part.to_owned(), 41 | }) 42 | } 43 | } 44 | 45 | pub fn parse_queries(base: InMemDicomObject, qs: &[T]) -> Result 46 | where 47 | T: AsRef, 48 | { 49 | let mut obj = base; 50 | 51 | for q in qs { 52 | let term_query: TermQuery = q.as_ref().parse()?; 53 | let v = term_to_value(term_query.selector.last_tag(), &term_query.match_value)?; 54 | obj.apply(AttributeOp::new( 55 | term_query.selector.clone(), 56 | AttributeAction::Set(v), 57 | )) 58 | .with_whatever_context(|_| { 59 | format!("could not set query attribute {}", &term_query.selector) 60 | })?; 61 | } 62 | Ok(obj) 63 | } 64 | 65 | fn term_to_value(tag: Tag, txt_value: &str) -> Result { 66 | if txt_value.is_empty() { 67 | return Ok(PrimitiveValue::Empty); 68 | } 69 | 70 | let vr = { 71 | StandardDataDictionary 72 | .by_tag(tag) 73 | .and_then(|e| e.vr.exact()) 74 | .unwrap_or(VR::LO) 75 | }; 76 | let value = match vr { 77 | VR::AE 78 | | VR::AS 79 | | VR::CS 80 | | VR::DA 81 | | VR::DS 82 | | VR::IS 83 | | VR::LO 84 | | VR::LT 85 | | VR::SH 86 | | VR::PN 87 | | VR::ST 88 | | VR::TM 89 | | VR::UI 90 | | VR::UC 91 | | VR::UR 92 | | VR::UT 93 | | VR::DT => PrimitiveValue::from(txt_value), 94 | VR::AT => whatever!("Unsupported VR AT"), 95 | VR::OB => whatever!("Unsupported VR OB"), 96 | VR::OD => whatever!("Unsupported VR OD"), 97 | VR::OF => whatever!("Unsupported VR OF"), 98 | VR::OL => whatever!("Unsupported VR OL"), 99 | VR::OV => whatever!("Unsupported VR OV"), 100 | VR::OW => whatever!("Unsupported VR OW"), 101 | VR::UN => whatever!("Unsupported VR UN"), 102 | VR::SQ => whatever!("Unsupported sequence-based query"), 103 | VR::SS => { 104 | let ss: i16 = txt_value 105 | .parse() 106 | .whatever_context("Failed to parse value as SS")?; 107 | PrimitiveValue::from(ss) 108 | } 109 | VR::SL => { 110 | let sl: i32 = txt_value 111 | .parse() 112 | .whatever_context("Failed to parse value as SL")?; 113 | PrimitiveValue::from(sl) 114 | } 115 | VR::SV => { 116 | let sv: i64 = txt_value 117 | .parse() 118 | .whatever_context("Failed to parse value as SV")?; 119 | PrimitiveValue::from(sv) 120 | } 121 | VR::US => { 122 | let us: u16 = txt_value 123 | .parse() 124 | .whatever_context("Failed to parse value as US")?; 125 | PrimitiveValue::from(us) 126 | } 127 | VR::UL => { 128 | let ul: u32 = txt_value 129 | .parse() 130 | .whatever_context("Failed to parse value as UL")?; 131 | PrimitiveValue::from(ul) 132 | } 133 | VR::UV => { 134 | let uv: u64 = txt_value 135 | .parse() 136 | .whatever_context("Failed to parse value as UV")?; 137 | PrimitiveValue::from(uv) 138 | } 139 | VR::FL => { 140 | let fl: f32 = txt_value 141 | .parse() 142 | .whatever_context("Failed to parse value as FL")?; 143 | PrimitiveValue::from(fl) 144 | } 145 | VR::FD => { 146 | let fd: f64 = txt_value 147 | .parse() 148 | .whatever_context("Failed to parse value as FD")?; 149 | PrimitiveValue::from(fd) 150 | } 151 | }; 152 | Ok(value) 153 | } 154 | -------------------------------------------------------------------------------- /fromimage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-fromimage" 3 | version = "0.8.0" 4 | edition = "2018" 5 | rust-version = "1.72.0" 6 | authors = ["Eduardo Pinho "] 7 | description = "A CLI tool for replacing the image content from DICOM files" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | categories = ["command-line-utilities"] 11 | keywords = ["cli", "dicom", "image", "image-conversion"] 12 | readme = "README.md" 13 | 14 | [features] 15 | default = ['dicom-object/inventory-registry'] 16 | 17 | [dependencies] 18 | clap = { version = "4.0.18", features = ["derive"] } 19 | dicom-core = { path = "../core", version = "0.8.1" } 20 | dicom-dictionary-std = { path = "../dictionary-std/", version = "0.8.0" } 21 | dicom-object = { path = "../object/", version = "0.8.1" } 22 | snafu = "0.8" 23 | tracing = "0.1.34" 24 | tracing-subscriber = "0.3.11" 25 | 26 | [dependencies.image] 27 | version = "0.25.1" 28 | default-features=false 29 | features = ["jpeg", "png", "pnm", "tiff", "webp", "bmp", "rayon", "exr"] 30 | -------------------------------------------------------------------------------- /fromimage/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `fromimage` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-fromimage.svg)](https://crates.io/crates/dicom-fromimage) 4 | [![Documentation](https://docs.rs/dicom-fromimage/badge.svg)](https://docs.rs/dicom-fromimage) 5 | 6 | This command line tool takes a base DICOM file of the image module 7 | and replaces the various DICOM attributes with those of another file. 8 | 9 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 10 | 11 | ## Usage 12 | 13 | ```none 14 | Usage: dicom-fromimage [OPTIONS] 15 | 16 | Arguments: 17 | Path to the base DICOM file to read 18 | Path to the image file to replace the DICOM file 19 | 20 | Options: 21 | -o, --out 22 | Path to the output image (default is to replace input extension with `.new.dcm`) 23 | --transfer-syntax 24 | Override the transfer syntax UID 25 | --encapsulate 26 | Encapsulate the image file raw data in a fragment sequence instead of writing native pixel data 27 | --retain-implementation 28 | Retain the implementation class UID and version name from base DICOM 29 | -v, --verbose 30 | Print more information about the image and the output file 31 | -h, --help 32 | Print help 33 | -V, --version 34 | Print version 35 | ``` 36 | 37 | ### Example 38 | 39 | Given a template DICOM file `base.dcm`, 40 | replace the image data with the image in `image.png`: 41 | 42 | ```none 43 | dicom-fromimage base.dcm image.png -o image.dcm 44 | ``` 45 | 46 | This will read the image file in the second argument 47 | and save it as native pixel data in Explicit VR Little Endian to `image.dcm`. 48 | 49 | You can also encapsulate the image file into a pixel data fragment, 50 | without converting to native pixel data. 51 | This allows you to create a DICOM file in JPEG baseline: 52 | 53 | ```none 54 | dicom-fromimage base.dcm image.jpg --transfer-syntax 1.2.840.10008.1.2.4.50 --encapsulate -o image.dcm 55 | ``` 56 | 57 | **Note:** `--transfer-syntax` is just a UID override, 58 | it will not automatically transcode the pixel data 59 | to conform to the given transfer syntax. 60 | To transcode files between transfer syntaxes, 61 | see [`dicom-transcode`](https://github.com/Enet4/dicom-rs/tree/master/pixeldata). 62 | -------------------------------------------------------------------------------- /json/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-json" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/Enet4/dicom-rs" 8 | description = "DICOM data serialization to/from JSON" 9 | keywords = ["dicom", "attributes", "json", "serialization"] 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | base64 = "0.22" 14 | dicom-core = { version = "0.8.1", path = "../core" } 15 | dicom-dictionary-std = { version = "0.8.0", path = "../dictionary-std" } 16 | dicom-object = { version = "0.8.0", path = "../object" } 17 | num-traits = "0.2.15" 18 | serde = { version = "1.0.164", features = ["derive"] } 19 | serde_json = { version = "1.0.96", features = ["preserve_order"] } 20 | tracing = "0.1.34" 21 | 22 | [dev-dependencies] 23 | dicom-test-files = "0.3" 24 | pretty_assertions = "1.3.0" 25 | -------------------------------------------------------------------------------- /json/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `json` 2 | 3 | [![crates.io](https://img.shields.io/crates/v/dicom-json.svg)](https://crates.io/crates/dicom-json) 4 | [![Documentation](https://docs.rs/dicom-json/badge.svg)](https://docs.rs/dicom-json) 5 | 6 | This sub-project is directed at users of the DICOM-rs ecosystem. 7 | It provides serialization of DICOM data to JSON 8 | and deserialization of JSON to DICOM data, 9 | as per the [DICOM standard part 18 chapter F][1]. 10 | 11 | [1]: https://dicom.nema.org/medical/dicom/current/output/chtml/part18/chapter_F.html 12 | 13 | This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 14 | -------------------------------------------------------------------------------- /json/src/de/value.rs: -------------------------------------------------------------------------------- 1 | //! DICOM value deserialization 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | use serde::Deserialize; 6 | 7 | #[derive(Debug, Clone, PartialEq, Deserialize)] 8 | pub struct DicomJsonPerson { 9 | #[serde(rename = "Alphabetic")] 10 | alphabetic: String, 11 | #[serde(rename = "Ideographic")] 12 | ideographic: Option, 13 | #[serde(rename = "Phonetic")] 14 | phonetic: Option, 15 | } 16 | 17 | impl fmt::Display for DicomJsonPerson { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | match self { 20 | DicomJsonPerson { 21 | alphabetic, 22 | ideographic: None, 23 | phonetic: None, 24 | } => write!(f, "{}", alphabetic), 25 | DicomJsonPerson { 26 | alphabetic, 27 | ideographic: Some(ideographic), 28 | phonetic: None, 29 | } => write!(f, "{}={}", alphabetic, ideographic), 30 | DicomJsonPerson { 31 | alphabetic, 32 | ideographic: None, 33 | phonetic: Some(phonetic), 34 | } => write!(f, "{}=={}", alphabetic, phonetic), 35 | DicomJsonPerson { 36 | alphabetic, 37 | ideographic: Some(ideographic), 38 | phonetic: Some(phonetic), 39 | } => write!(f, "{}={}={}", alphabetic, ideographic, phonetic), 40 | } 41 | } 42 | } 43 | 44 | #[derive(Debug, Clone, PartialEq, Deserialize)] 45 | pub struct BulkDataUri(String); 46 | 47 | #[derive(Debug, Clone, PartialEq, Deserialize)] 48 | #[serde(untagged)] 49 | pub enum NumberOrText { 50 | Number(N), 51 | Text(String), 52 | } 53 | 54 | impl NumberOrText 55 | where 56 | N: Clone, 57 | N: FromStr, 58 | { 59 | pub fn to_num(&self) -> Result::Err> { 60 | match self { 61 | NumberOrText::Number(num) => Ok(num.clone()), 62 | NumberOrText::Text(text) => text.parse(), 63 | } 64 | } 65 | } 66 | 67 | impl std::fmt::Display for NumberOrText 68 | where 69 | N: std::fmt::Display, 70 | { 71 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 72 | match self { 73 | NumberOrText::Number(number) => std::fmt::Display::fmt(number, f), 74 | NumberOrText::Text(text) => f.write_str(text), 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /object/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-object" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | edition = "2018" 6 | rust-version = "1.72.0" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/Enet4/dicom-rs" 9 | description = "A high-level API for reading and manipulating DICOM objects" 10 | keywords = ["dicom", "object", "attributes"] 11 | readme = "README.md" 12 | 13 | [features] 14 | default = [] 15 | inventory-registry = ['dicom-encoding/inventory-registry', 'dicom-transfer-syntax-registry/inventory-registry'] 16 | 17 | [dependencies] 18 | dicom-core = { path = "../core", version = "0.8.1" } 19 | dicom-encoding = { path = "../encoding", version = "0.8.1" } 20 | dicom-parser = { path = "../parser", version = "0.8.1" } 21 | dicom-dictionary-std = { path = "../dictionary-std", version = "0.8.0" } 22 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry", version = "0.8.1" } 23 | itertools = "0.14" 24 | byteordered = "0.6" 25 | smallvec = "1.6.1" 26 | snafu = "0.8" 27 | tracing = "0.1.34" 28 | 29 | [dev-dependencies] 30 | tempfile = "3.2.0" 31 | dicom-test-files = "0.3" 32 | -------------------------------------------------------------------------------- /object/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `object` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-object.svg)](https://crates.io/crates/dicom-object) 4 | [![Documentation](https://docs.rs/dicom-object/badge.svg)](https://docs.rs/dicom-object) 5 | 6 | This sub-project is directed at users of the DICOM-rs ecosystem. It provides a high-level 7 | abstraction to DICOM objects, enabling objects to be retrieved from files or 8 | ['readers'](https://doc.rust-lang.org/std/io/trait.Read.html), and then analysed as a tree 9 | of attributes. 10 | 11 | This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project 12 | and is contained by the parent crate [`dicom`](https://crates.io/crates/dicom). 13 | -------------------------------------------------------------------------------- /object/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | -------------------------------------------------------------------------------- /object/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-object-fuzz" 3 | version = "0.0.0" 4 | description = "Fuzz testing for the dicom-object crate" 5 | authors = [] 6 | publish = false 7 | edition = "2021" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.dicom-object] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "open_file" 24 | path = "fuzz_targets/open_file.rs" 25 | test = false 26 | doc = false 27 | 28 | [profile.release] 29 | debug = true 30 | -------------------------------------------------------------------------------- /object/fuzz/fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | env ASAN_OPTIONS=allocator_may_return_null=1:max_allocation_size_mb=40 cargo +nightly fuzz run open_file 4 | -------------------------------------------------------------------------------- /object/fuzz/fuzz_targets/open_file.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::{fuzz_target, Corpus}; 3 | use std::error::Error; 4 | 5 | fuzz_target!(|data: &[u8]| -> Corpus { 6 | match fuzz(data) { 7 | Ok(_) => Corpus::Keep, 8 | Err(_) => Corpus::Reject, 9 | } 10 | }); 11 | 12 | fn fuzz(data: &[u8]) -> Result<(), Box> { 13 | // deserialize random bytes 14 | let mut obj = dicom_object::OpenFileOptions::new() 15 | .read_preamble(dicom_object::file::ReadPreamble::Auto) 16 | .odd_length_strategy(dicom_object::file::OddLengthStrategy::Fail) 17 | .from_reader(data)?; 18 | 19 | // remove group length elements 20 | for g in 0..=0x07FF { 21 | obj.remove_element(dicom_object::Tag(g, 0x0000)); 22 | } 23 | // serialize object back to bytes 24 | let mut bytes = Vec::new(); 25 | obj.write_all(&mut bytes) 26 | .expect("writing DICOM file should always be successful"); 27 | 28 | // deserialize back to object 29 | let obj2 = dicom_object::from_reader(bytes.as_slice()) 30 | .expect("serialized object should always deserialize"); 31 | 32 | // assert equivalence 33 | assert_eq!(obj, obj2); 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /object/src/attribute.rs: -------------------------------------------------------------------------------- 1 | use dicom_core::{Tag, VR, header::DataElementHeader}; 2 | use std::io::Read; 3 | 4 | /// Abstraction for any attribute in a DICOM object. 5 | pub trait Attribute<'a> { 6 | type Error; 7 | type Reader: 'a + Read; 8 | type Item: 'a; 9 | type ItemIter: IntoIterator; 10 | 11 | /// Retrieve the header information of this attribute. 12 | fn header(&self) -> DataElementHeader; 13 | 14 | /// Retrieve the value representation. 15 | fn vr(&self) -> VR { 16 | self.header().vr 17 | } 18 | 19 | /// Retrieve the tag. 20 | fn tag(&self) -> Tag { 21 | self.header().tag 22 | } 23 | 24 | /// Read the entire value as a single string. 25 | fn str(&self) -> Result<&'a str, Self::Error>; 26 | 27 | /// Read the entire value as raw bytes. 28 | fn raw_bytes(&self) -> Result<&'a [u8], Self::Error>; 29 | 30 | /// Create a new byte reader for the value of this attribute. 31 | fn stream(&self) -> Result; 32 | } 33 | -------------------------------------------------------------------------------- /object/src/file.rs: -------------------------------------------------------------------------------- 1 | use dicom_core::{DataDictionary, Tag}; 2 | use dicom_dictionary_std::StandardDataDictionary; 3 | use dicom_encoding::transfer_syntax::TransferSyntaxIndex; 4 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 5 | 6 | // re-export from dicom_parser 7 | pub use dicom_parser::dataset::read::OddLengthStrategy; 8 | 9 | use crate::{DefaultDicomObject, ReadError}; 10 | use std::io::Read; 11 | use std::path::Path; 12 | 13 | pub type Result = std::result::Result; 14 | 15 | /// Create a DICOM object by reading from a byte source. 16 | /// 17 | /// This function assumes the standard file encoding structure without the 18 | /// preamble: file meta group, followed by the rest of the data set. 19 | pub fn from_reader(file: F) -> Result 20 | where 21 | F: Read, 22 | { 23 | OpenFileOptions::new().from_reader(file) 24 | } 25 | 26 | /// Create a DICOM object by reading from a file. 27 | /// 28 | /// This function assumes the standard file encoding structure: 128-byte 29 | /// preamble, file meta group, and the rest of the data set. 30 | pub fn open_file

(path: P) -> Result 31 | where 32 | P: AsRef, 33 | { 34 | OpenFileOptions::new().open_file(path) 35 | } 36 | 37 | /// A builder type for opening a DICOM file with additional options. 38 | /// 39 | /// This builder exposes additional properties 40 | /// to configure the reading of a DICOM file. 41 | /// 42 | /// # Example 43 | /// 44 | /// Create a `OpenFileOptions`, 45 | /// call adaptor methods in a chain, 46 | /// and finish the operation with 47 | /// either [`open_file()`](OpenFileOptions::open_file) 48 | /// or [`from_reader()`](OpenFileOptions::from_reader). 49 | /// 50 | /// ```no_run 51 | /// # use dicom_object::OpenFileOptions; 52 | /// let file = OpenFileOptions::new() 53 | /// .read_until(dicom_dictionary_std::tags::PIXEL_DATA) 54 | /// .open_file("path/to/file.dcm")?; 55 | /// # Result::<(), Box>::Ok(()) 56 | /// ``` 57 | #[derive(Debug, Default, Clone)] 58 | #[non_exhaustive] 59 | pub struct OpenFileOptions { 60 | data_dictionary: D, 61 | ts_index: T, 62 | read_until: Option, 63 | read_preamble: ReadPreamble, 64 | odd_length: OddLengthStrategy, 65 | } 66 | 67 | impl OpenFileOptions { 68 | pub fn new() -> Self { 69 | OpenFileOptions::default() 70 | } 71 | } 72 | 73 | impl OpenFileOptions { 74 | /// Set the operation to read only until the given tag is found. 75 | /// 76 | /// The reading process ends immediately after this tag, 77 | /// or any other tag that is next in the standard DICOM tag ordering, 78 | /// is found in the object's root data set. 79 | /// An element with the exact tag will be excluded from the output. 80 | pub fn read_until(mut self, tag: Tag) -> Self { 81 | self.read_until = Some(tag); 82 | self 83 | } 84 | 85 | /// Set the operation to read all elements of the data set to the end. 86 | /// 87 | /// This is the default behavior. 88 | pub fn read_all(mut self) -> Self { 89 | self.read_until = None; 90 | self 91 | } 92 | 93 | /// Set whether to read the 128-byte DICOM file preamble. 94 | pub fn read_preamble(mut self, option: ReadPreamble) -> Self { 95 | self.read_preamble = option; 96 | self 97 | } 98 | 99 | /// Set how data elements with an odd length should be handled. 100 | pub fn odd_length_strategy(mut self, option: OddLengthStrategy) -> Self { 101 | self.odd_length = option; 102 | self 103 | } 104 | 105 | /// Set the transfer syntax index to use when reading the file. 106 | pub fn transfer_syntax_index(self, ts_index: Tr) -> OpenFileOptions 107 | where 108 | Tr: TransferSyntaxIndex, 109 | { 110 | OpenFileOptions { 111 | data_dictionary: self.data_dictionary, 112 | read_until: self.read_until, 113 | read_preamble: self.read_preamble, 114 | ts_index, 115 | odd_length: self.odd_length, 116 | } 117 | } 118 | 119 | /// Set the transfer syntax index to use when reading the file. 120 | #[deprecated(since="0.8.1", note="please use `transfer_syntax_index` instead")] 121 | pub fn tranfer_syntax_index(self, ts_index: Tr) -> OpenFileOptions 122 | where 123 | Tr: TransferSyntaxIndex, 124 | { 125 | self.transfer_syntax_index(ts_index) 126 | } 127 | 128 | /// Set the data element dictionary to use when reading the file. 129 | pub fn dictionary(self, dict: Di) -> OpenFileOptions 130 | where 131 | Di: DataDictionary, 132 | Di: Clone, 133 | { 134 | OpenFileOptions { 135 | data_dictionary: dict, 136 | read_until: self.read_until, 137 | read_preamble: self.read_preamble, 138 | ts_index: self.ts_index, 139 | odd_length: self.odd_length, 140 | } 141 | } 142 | 143 | /// Open the file at the given path. 144 | pub fn open_file

(self, path: P) -> Result> 145 | where 146 | P: AsRef, 147 | D: DataDictionary, 148 | D: Clone, 149 | T: TransferSyntaxIndex, 150 | { 151 | DefaultDicomObject::open_file_with_all_options( 152 | path, 153 | self.data_dictionary, 154 | self.ts_index, 155 | self.read_until, 156 | self.read_preamble, 157 | self.odd_length, 158 | ) 159 | } 160 | 161 | /// Obtain a DICOM object by reading from a byte source. 162 | /// 163 | /// This method assumes 164 | /// the standard file encoding structure without the preamble: 165 | /// file meta group, followed by the rest of the data set. 166 | pub fn from_reader(self, from: R) -> Result> 167 | where 168 | R: Read, 169 | D: DataDictionary, 170 | D: Clone, 171 | T: TransferSyntaxIndex, 172 | { 173 | DefaultDicomObject::from_reader_with_all_options( 174 | from, 175 | self.data_dictionary, 176 | self.ts_index, 177 | self.read_until, 178 | self.read_preamble, 179 | self.odd_length, 180 | ) 181 | } 182 | } 183 | 184 | /// An enumerate of supported options for 185 | /// whether to read the 128-byte DICOM file preamble. 186 | #[derive(Debug, Default, Copy, Clone, Eq, Hash, PartialEq)] 187 | pub enum ReadPreamble { 188 | /// Try to detect the presence of the preamble automatically. 189 | /// If detection fails, it will revert to always reading the preamble 190 | /// when opening a file by path, 191 | /// and not reading it when reading from a byte source. 192 | #[default] 193 | Auto, 194 | /// Never read the preamble, 195 | /// thus assuming that the original source does not have it. 196 | Never, 197 | /// Always read the preamble first, 198 | /// thus assuming that the original source always has it. 199 | Always, 200 | } 201 | -------------------------------------------------------------------------------- /object/src/ops.rs: -------------------------------------------------------------------------------- 1 | //! Baseline attribute operation implementations. 2 | //! 3 | //! See the [`dicom_core::ops`] module 4 | //! for more information. 5 | 6 | use dicom_core::ops::{ApplyOp, AttributeOp, AttributeSelector, AttributeSelectorStep}; 7 | use dicom_core::value::{ModifyValueError, ValueType}; 8 | use dicom_core::Tag; 9 | use snafu::Snafu; 10 | 11 | use crate::FileDicomObject; 12 | 13 | /// An error which may occur when applying an attribute operation to an object. 14 | #[derive(Debug, Snafu)] 15 | #[non_exhaustive] 16 | #[snafu(visibility(pub(crate)))] 17 | pub enum ApplyError { 18 | /// Missing intermediate sequence for {selector} at step {step_index} 19 | MissingSequence { 20 | selector: AttributeSelector, 21 | step_index: u32, 22 | }, 23 | /// Step {step_index} for {selector} is not a data set sequence 24 | NotASequence { 25 | selector: AttributeSelector, 26 | step_index: u32, 27 | }, 28 | /// Incompatible source element type {kind:?} for extension 29 | IncompatibleTypes { 30 | /// the source element value type 31 | kind: ValueType, 32 | }, 33 | /// Illegal removal of mandatory attribute 34 | Mandatory, 35 | /// Could not modify source element type through extension 36 | Modify { source: ModifyValueError }, 37 | /// Illegal extension of fixed cardinality attribute 38 | IllegalExtend, 39 | /// Unsupported action 40 | UnsupportedAction, 41 | /// Unsupported attribute insertion 42 | UnsupportedAttribute, 43 | } 44 | 45 | /// Result type for when applying attribute operations to an object. 46 | pub type ApplyResult = std::result::Result; 47 | 48 | impl ApplyOp for FileDicomObject 49 | where 50 | T: ApplyOp, 51 | { 52 | type Err = ApplyError; 53 | 54 | /// Apply the given attribute operation on this object. 55 | /// 56 | /// The operation is delegated to the file meta table 57 | /// if the selector root tag is in group `0002`, 58 | /// and to the underlying object otherwise. 59 | /// 60 | /// See the [`dicom_core::ops`] module 61 | /// for more information. 62 | fn apply(&mut self, op: AttributeOp) -> ApplyResult { 63 | if let AttributeSelectorStep::Tag(Tag(0x0002, _)) = op.selector.first_step() { 64 | self.meta.apply(op) 65 | } else { 66 | self.obj.apply(op) 67 | } 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use dicom_core::ops::{ApplyOp, AttributeAction, AttributeOp}; 74 | use dicom_core::{DataElement, PrimitiveValue, VR}; 75 | 76 | use crate::{FileMetaTableBuilder, InMemDicomObject}; 77 | 78 | /// Attribute operations can be applied on a `FileDicomObject` 79 | #[test] 80 | fn file_dicom_object_can_apply_op() { 81 | let mut obj = InMemDicomObject::new_empty(); 82 | 83 | obj.put(DataElement::new( 84 | dicom_dictionary_std::tags::PATIENT_NAME, 85 | VR::PN, 86 | PrimitiveValue::from("John Doe"), 87 | )); 88 | 89 | let mut obj = obj 90 | .with_meta( 91 | FileMetaTableBuilder::new() 92 | .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7") 93 | .media_storage_sop_instance_uid("1.2.23456789") 94 | .transfer_syntax("1.2.840.10008.1.2.1"), 95 | ) 96 | .unwrap(); 97 | 98 | // apply operation on main data set 99 | obj.apply(AttributeOp { 100 | selector: dicom_dictionary_std::tags::PATIENT_NAME.into(), 101 | action: AttributeAction::SetStr("Patient^Anonymous".into()), 102 | }) 103 | .unwrap(); 104 | 105 | // contains new patient name 106 | assert_eq!( 107 | obj.element(dicom_dictionary_std::tags::PATIENT_NAME) 108 | .unwrap() 109 | .value() 110 | .to_str() 111 | .unwrap(), 112 | "Patient^Anonymous", 113 | ); 114 | 115 | // apply operation on file meta information 116 | obj.apply(AttributeOp { 117 | selector: dicom_dictionary_std::tags::MEDIA_STORAGE_SOP_INSTANCE_UID.into(), 118 | action: AttributeAction::SetStr("2.25.153241429675951194530939969687300037165".into()), 119 | }) 120 | .unwrap(); 121 | 122 | // file meta table contains new SOP instance UID 123 | assert_eq!( 124 | obj.meta().media_storage_sop_instance_uid(), 125 | "2.25.153241429675951194530939969687300037165", 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /object/src/tokens.rs: -------------------------------------------------------------------------------- 1 | //! Conversion of DICOM objects into tokens. 2 | use crate::mem::InMemDicomObject; 3 | use dicom_core::DataElement; 4 | use dicom_parser::dataset::{DataToken, IntoTokens, IntoTokensOptions}; 5 | use std::collections::VecDeque; 6 | 7 | /// A stream of tokens from a DICOM object. 8 | pub struct InMemObjectTokens { 9 | /// iterators of tokens in order of priority. 10 | tokens_pending: VecDeque, 11 | /// the iterator of data elements in order. 12 | elem_iter: E, 13 | /// whether the tokens are done 14 | fused: bool, 15 | /// Options to take into account when generating tokens 16 | token_options: IntoTokensOptions, 17 | } 18 | 19 | impl InMemObjectTokens 20 | where 21 | E: Iterator, 22 | { 23 | pub fn new(obj: T) -> Self 24 | where 25 | T: IntoIterator, 26 | { 27 | InMemObjectTokens { 28 | tokens_pending: Default::default(), 29 | elem_iter: obj.into_iter(), 30 | fused: false, 31 | token_options: Default::default(), 32 | } 33 | } 34 | 35 | pub fn new_with_options(obj: T, token_options: IntoTokensOptions) -> Self 36 | where 37 | T: IntoIterator, 38 | { 39 | InMemObjectTokens { 40 | tokens_pending: Default::default(), 41 | elem_iter: obj.into_iter(), 42 | fused: false, 43 | token_options, 44 | } 45 | } 46 | } 47 | 48 | impl Iterator for InMemObjectTokens 49 | where 50 | E: Iterator>, 51 | E::Item: IntoTokens, 52 | { 53 | type Item = DataToken; 54 | 55 | fn next(&mut self) -> Option { 56 | if self.fused { 57 | return None; 58 | } 59 | 60 | // otherwise, consume pending tokens 61 | if let Some(token) = self.tokens_pending.pop_front() { 62 | return Some(token); 63 | } 64 | 65 | // otherwise, expand next element, recurse 66 | if let Some(elem) = self.elem_iter.next() { 67 | self.tokens_pending = if self.token_options == Default::default() { 68 | elem.into_tokens() 69 | } else { 70 | elem.into_tokens_with_options(self.token_options) 71 | } 72 | .collect(); 73 | 74 | self.next() 75 | } else { 76 | // no more elements 77 | None 78 | } 79 | } 80 | 81 | fn size_hint(&self) -> (usize, Option) { 82 | // make a slightly better estimation for the minimum 83 | // number of tokens that follow: 2 tokens per element left 84 | (self.elem_iter.size_hint().0 * 2, None) 85 | } 86 | } 87 | 88 | impl IntoTokens for InMemDicomObject { 89 | type Iter = InMemObjectTokens< as IntoIterator>::IntoIter>; 90 | 91 | fn into_tokens(self) -> Self::Iter { 92 | InMemObjectTokens::new(self) 93 | } 94 | 95 | fn into_tokens_with_options(self, mut options: IntoTokensOptions) -> Self::Iter { 96 | //This is required for recursing with the correct option 97 | options.force_invalidate_sq_length |= self.charset_changed; 98 | InMemObjectTokens::new_with_options(self, options) 99 | } 100 | } 101 | 102 | impl<'a, D> IntoTokens for &'a InMemDicomObject 103 | where 104 | D: Clone, 105 | { 106 | type Iter = 107 | InMemObjectTokens as IntoIterator>::IntoIter>>; 108 | 109 | fn into_tokens(self) -> Self::Iter { 110 | self.into_tokens_with_options(Default::default()) 111 | } 112 | 113 | fn into_tokens_with_options(self, mut options: IntoTokensOptions) -> Self::Iter { 114 | options.force_invalidate_sq_length |= self.charset_changed; 115 | 116 | InMemObjectTokens::new_with_options(self.into_iter().cloned(), options) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /object/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufReader, Read}, 4 | }; 5 | 6 | use dicom_core::value::Value; 7 | use dicom_dictionary_std::tags; 8 | use dicom_encoding::text::SpecificCharacterSet; 9 | use dicom_object::{ 10 | file::{OpenFileOptions, ReadPreamble}, 11 | mem::InMemDicomObject, 12 | open_file, 13 | }; 14 | #[test] 15 | fn test_ob_value_with_unknown_length() { 16 | let path = 17 | dicom_test_files::path("pydicom/JPEG2000.dcm").expect("test DICOM file should exist"); 18 | let object = open_file(&path).unwrap(); 19 | let element = object.element_by_name("PixelData").unwrap(); 20 | 21 | match element.value() { 22 | Value::PixelSequence(seq) => { 23 | let fragments = seq.fragments(); 24 | // check offset table 25 | assert_eq!(seq.offset_table().len(), 0); 26 | 27 | // check if the leading and trailing bytes look right 28 | assert_eq!(fragments.len(), 1); 29 | let fragment = &fragments[0]; 30 | assert_eq!(fragment[0..2], [255, 79]); 31 | assert_eq!(fragment[fragment.len() - 2..fragment.len()], [255, 217]); 32 | } 33 | value => { 34 | panic!("expected a pixel sequence, but got {:?}", value); 35 | } 36 | } 37 | } 38 | 39 | #[test] 40 | fn test_read_until_pixel_data() { 41 | let path = 42 | dicom_test_files::path("pydicom/JPEG2000.dcm").expect("test DICOM file should exist"); 43 | let object = OpenFileOptions::new() 44 | .read_until(tags::PIXEL_DATA) 45 | .open_file(&path) 46 | .expect("File should open successfully"); 47 | 48 | // contains other elements such as modality 49 | let element = object.element(tags::MODALITY).unwrap(); 50 | assert_eq!(element.value().to_str().unwrap(), "NM"); 51 | 52 | // but does not contain pixel data 53 | assert!(matches!( 54 | object.element(tags::PIXEL_DATA), 55 | Err(dicom_object::AccessError::NoSuchDataElementTag { .. }) 56 | )); 57 | } 58 | 59 | #[test] 60 | fn test_read_data_with_preamble() { 61 | let path = dicom_test_files::path("pydicom/liver.dcm").expect("test DICOM file should exist"); 62 | let source = BufReader::new(File::open(path).unwrap()); 63 | 64 | // should read preamble even though it's from a reader 65 | let object = OpenFileOptions::new() 66 | .read_preamble(ReadPreamble::Always) 67 | .from_reader(source) 68 | .expect("Should read from source successfully"); 69 | 70 | // contains elements such as study date 71 | let element = object.element(tags::STUDY_DATE).unwrap(); 72 | assert_eq!(element.value().to_str().unwrap(), "20030417"); 73 | } 74 | 75 | #[test] 76 | fn test_read_data_with_preamble_auto() { 77 | let path = dicom_test_files::path("pydicom/liver.dcm").expect("test DICOM file should exist"); 78 | let source = BufReader::new(File::open(path).unwrap()); 79 | 80 | // should read preamble even though it's from a reader 81 | let object = OpenFileOptions::new() 82 | .from_reader(source) 83 | .expect("Should read from source successfully"); 84 | 85 | // contains elements such as study date 86 | let element = object.element(tags::STUDY_DATE).unwrap(); 87 | assert_eq!(element.value().to_str().unwrap(), "20030417"); 88 | } 89 | 90 | #[test] 91 | fn test_read_data_without_preamble() { 92 | let path = dicom_test_files::path("pydicom/liver.dcm").expect("test DICOM file should exist"); 93 | let mut source = BufReader::new(File::open(path).unwrap()); 94 | 95 | // read preamble manually 96 | let mut preamble = [0; 128]; 97 | 98 | source.read_exact(&mut preamble).unwrap(); 99 | 100 | // explicitly do not read preamble 101 | let object = OpenFileOptions::new() 102 | .read_preamble(ReadPreamble::Never) 103 | .from_reader(source) 104 | .expect("Should read from source successfully"); 105 | 106 | // contains elements such as study date 107 | let element = object.element(tags::STUDY_DATE).unwrap(); 108 | assert_eq!(element.value().to_str().unwrap(), "20030417"); 109 | } 110 | 111 | #[test] 112 | fn test_read_data_without_preamble_auto() { 113 | let path = dicom_test_files::path("pydicom/liver.dcm").expect("test DICOM file should exist"); 114 | let mut source = BufReader::new(File::open(path).unwrap()); 115 | 116 | // skip preamble 117 | let mut preamble = [0; 128]; 118 | 119 | source.read_exact(&mut preamble).unwrap(); 120 | 121 | // detect lack of preamble automatically 122 | let object = OpenFileOptions::new() 123 | .from_reader(source) 124 | .expect("Should read from source successfully"); 125 | 126 | // contains elements such as study date 127 | let element = object.element(tags::STUDY_DATE).unwrap(); 128 | assert_eq!(element.value().to_str().unwrap(), "20030417"); 129 | } 130 | 131 | #[test] 132 | fn test_expl_vr_le_no_meta() { 133 | let path = dicom_test_files::path("pydicom/ExplVR_LitEndNoMeta.dcm") 134 | .expect("test DICOM file should exist"); 135 | let source = BufReader::new(File::open(path).unwrap()); 136 | let ts = dicom_transfer_syntax_registry::entries::EXPLICIT_VR_LITTLE_ENDIAN.erased(); 137 | let object = 138 | InMemDicomObject::read_dataset_with_ts_cs(source, &ts, SpecificCharacterSet::default()) 139 | .unwrap(); 140 | 141 | let sop_instance_uid = object.element_by_name("SOPInstanceUID").unwrap(); 142 | assert_eq!(sop_instance_uid.to_str().unwrap(), "1.2.333.4444.5.6.7.8",); 143 | 144 | let series_instance_uid = object.element_by_name("SeriesInstanceUID").unwrap(); 145 | assert_eq!( 146 | series_instance_uid.to_str().unwrap(), 147 | "1.2.333.4444.5.6.7.8.99", 148 | ); 149 | 150 | let frame_of_reference_uid = object.element_by_name("FrameOfReferenceUID").unwrap(); 151 | assert_eq!( 152 | frame_of_reference_uid.to_str().unwrap(), 153 | "1.2.333.4444.5.6.7.8.9", 154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /parent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | description = "A pure Rust implementation of the DICOM standard" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | readme = "README.md" 11 | 12 | [badges] 13 | maintenance = { status = "actively-developed" } 14 | 15 | [features] 16 | default = ['inventory-registry', 'ul', 'pixeldata'] 17 | inventory-registry = ['dicom-encoding/inventory-registry', 'dicom-transfer-syntax-registry/inventory-registry'] 18 | ul = ['dicom-ul'] 19 | pixeldata = ['dicom-pixeldata'] 20 | image = ["pixeldata", "dicom-pixeldata/image"] 21 | ndarray = ["pixeldata", "dicom-pixeldata/ndarray"] 22 | 23 | [dependencies] 24 | dicom-core = { path = "../core", version = "0.8.1" } 25 | dicom-dictionary-std = { path = "../dictionary-std", version = "0.8.0" } 26 | dicom-dump = { version = "0.8.0", path = "../dump", default-features = false } 27 | dicom-encoding = { path = "../encoding", version = "0.8.1" } 28 | dicom-parser = { path = "../parser", version = "0.8.1" } 29 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry", version = "0.8.1" } 30 | dicom-object = { path = "../object", version = "0.8.1", default-features = false } 31 | dicom-ul = { optional = true, path = "../ul", version = "0.8.1" } 32 | dicom-pixeldata = { optional = true, path = "../pixeldata", version = "0.8.1" } 33 | -------------------------------------------------------------------------------- /parent/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom.svg)](https://crates.io/crates/dicom) 4 | [![Documentation](https://docs.rs/dicom/badge.svg)](https://docs.rs/dicom) 5 | 6 | `dicom` is a library for the [DICOM] standard. 7 | It is part of the [DICOM-rs] project, 8 | an ecosystem of modules and tools for DICOM compliant systems. 9 | 10 | This collection provides a pure Rust implementation of the DICOM standard, 11 | allowing users to read and write DICOM data over files and other sources, 12 | while remaining intrinsically efficient, fast, intuitive, and safe to use. 13 | 14 | ## Using as a library 15 | 16 | This crate exposes the [`dicom-object`] crate directly via the `object` module, 17 | which has a high-level API for reading, writing, and manipulating DICOM objects. 18 | Other key components of the full library are available in this one as well, 19 | albeit representing different levels of abstraction. 20 | 21 | An example of use follows. 22 | For more details, please visit the [`dicom-object` documentation] 23 | or the [full library documentation]. 24 | 25 | ```rust 26 | use dicom::core::Tag; 27 | use dicom::object::{open_file, Result}; 28 | 29 | let obj = open_file("0001.dcm")?; 30 | let patient_name = obj.element_by_name("PatientName")?.to_str()?; 31 | let modality = obj.element_by_name("Modality")?.to_str()?; 32 | let pixel_data_bytes = obj.element(Tag(0x7FE0, 0x0010))?.to_bytes()?; 33 | ``` 34 | 35 | ### Cargo features 36 | 37 | This crate enables the inventory-based transfer syntax registry by default, 38 | which allows for a seamless integration of additional transfer syntaxes 39 | without changing the application. 40 | In environments which do not support this, the feature can be disabled. 41 | Please see the documentation of [`dicom-transfer-syntax-registry`] 42 | for more information. 43 | 44 | The following root modules are behind Cargo features enabled by default: 45 | 46 | - [`ul`]: the DICOM upper layer protocol library 47 | - [`pixeldata`]: the pixel data abstraction library. 48 | The Cargo features `image`, `ndarray` are re-exported from `dicom-pixeldata` 49 | and may be enabled at will through the parent crate. 50 | 51 | If you do not intend to use these modules, 52 | you can disable these features accordingly. 53 | 54 | [DICOM]: https://dicomstandard.org 55 | [DICOM-rs]: https://github.com/Enet4/dicom-rs 56 | [`dicom-transfer-syntax-registry`]: https://docs.rs/dicom-transfer-syntax-registry 57 | [`dicom-object`]: https://crates.io/crates/dicom-object 58 | [`dicom-object` documentation]: https://docs.rs/dicom-object 59 | [`ul`]: https://crates.io/crates/dicom-ul 60 | [`pixeldata`]: https://crates.io/crates/dicom-pixeldata 61 | [full library documentation]: https://docs.rs/dicom 62 | -------------------------------------------------------------------------------- /parent/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # DICOM-rs library 2 | //! 3 | //! This crate serves as a mother library for 4 | //! building DICOM compliant systems. 5 | //! 6 | //! This library aggregates the key modules 7 | //! that you are likely to require when building software using DICOM-rs. 8 | //! These modules are also available as crates 9 | //! which can be fetched independently, 10 | //! in complement or as an alternative to using the `dicom` crate. 11 | //! When adding a new dependency in the DICOM-rs umbrella, 12 | //! they will generally have the `dicom-` prefix. 13 | //! For instance, the module `object` 14 | //! lives in the crate named [`dicom-object`][1]. 15 | //! 16 | //! [1]: https://docs.rs/dicom-object 17 | //! 18 | //! ## Basic 19 | //! 20 | //! - For an idiomatic API to reading and writing DICOM data 21 | //! from files or other sources, 22 | //! see the [`object`] module. 23 | //! - To print human readable summaries of a DICOM object, 24 | //! see the [`dump`] module. 25 | //! - The [`pixeldata`] module helps you convert pixel data 26 | //! into images or multi-dimensional arrays. 27 | //! - The [`core`] crate contains most of the data types 28 | //! that the other crates rely on, 29 | //! including types for DICOM Tags ([`Tag`](dicom_core::Tag)), 30 | //! value representations ([`VR`](dicom_core::VR)), 31 | //! and in-memory representations of [DICOM values](dicom_core::DicomValue), 32 | //! contained in [data elements](dicom_core::DataElement). 33 | //! For convenience, the [`dicom_value!`] macro 34 | //! has been re-exported here as well. 35 | //! - The DICOM standard data dictionary is in [`dictionary_std`], 36 | //! which not only provides a singleton to a standard DICOM tag index 37 | //! that can be queried at run-time, 38 | //! it also provides constants for known tags 39 | //! in the [`tags`][dictionary_std::tags] module. 40 | //! - In the event that you need to get 41 | //! the global registry of known transfer syntaxes, 42 | //! [`transfer_syntax`] a re-export of the `dicom-transfer-syntax-registry` crate. 43 | //! Moreover, [inventory-based transfer syntax registry][ts] 44 | //! is enabled by default 45 | //! (see the link for more information). 46 | //! 47 | //! [ts]: dicom_encoding::transfer_syntax 48 | //! 49 | //! ## Advanced 50 | //! 51 | //! - To write DICOM network application entity software, 52 | //! see the [`ul`] module for PDU reading/writing 53 | //! and a DICOM association API. 54 | //! - If you are writing or declaring your own transfer syntax, 55 | //! you will need to take the [`encoding`] module 56 | //! and build your own [`TransferSyntax`](encoding::TransferSyntax) implementation. 57 | //! - [`parser`] contains the mid-level abstractions for 58 | //! reading and writing DICOM data sets. 59 | //! It might only be truly needed if 60 | //! the `object` API is unfit or too inefficient for a certain task. 61 | //! 62 | //! ## More 63 | //! 64 | //! See the [DICOM-rs project repository][2] 65 | //! for the full list of crates available in the DICOM-rs ecosystem. 66 | //! 67 | //! [2]: https://github.com/Enet4/dicom-rs 68 | pub use dicom_core as core; 69 | pub use dicom_dictionary_std as dictionary_std; 70 | pub use dicom_dump as dump; 71 | pub use dicom_encoding as encoding; 72 | pub use dicom_object as object; 73 | pub use dicom_parser as parser; 74 | #[cfg(feature = "pixeldata")] 75 | pub use dicom_pixeldata as pixeldata; 76 | pub use dicom_transfer_syntax_registry as transfer_syntax; 77 | #[cfg(feature = "ul")] 78 | pub use dicom_ul as ul; 79 | 80 | // re-export dicom_value macro 81 | pub use dicom_core::dicom_value; 82 | -------------------------------------------------------------------------------- /parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-parser" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | description = "A middle-level parser and printer of DICOM data sets" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | categories = ["parser-implementations"] 11 | keywords = ["dicom", "parser"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | dicom-core = { path = "../core", version = "0.8.1" } 16 | dicom-encoding = { path = "../encoding", version = "0.8.1" } 17 | dicom-dictionary-std = { path = "../dictionary-std/", version = "0.8.0" } 18 | smallvec = "1.6.1" 19 | snafu = "0.8" 20 | tracing = "0.1.34" 21 | -------------------------------------------------------------------------------- /parser/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `parser` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-parser.svg)](https://crates.io/crates/dicom-parser) 4 | [![Documentation](https://docs.rs/dicom-parser/badge.svg)](https://docs.rs/dicom-parser) 5 | 6 | This sub-project implements a middle-level abstraction for parsing and printing DICOM data sets through a sequence of tokens. 7 | 8 | Please see [`dicom-object`](https://crates.io/crates/dicom-object) for a higher-level API. 9 | 10 | This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project 11 | and is contained by the parent crate [`dicom`](https://crates.io/crates/dicom). 12 | -------------------------------------------------------------------------------- /parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::derive_partial_eq_without_eq)] 2 | //! This crate works on top of DICOM encoding primitives to provide transfer 3 | //! syntax resolution and abstraction for parsing DICOM data sets, which 4 | //! ultimately enables the user to perceive the DICOM object as a sequence of 5 | //! tokens. 6 | //! 7 | //! For the time being, all APIs are based on synchronous I/O. 8 | //! 9 | //! For a more intuitive, object-oriented API, please see the `dicom-object` 10 | //! crate. 11 | pub mod dataset; 12 | pub mod stateful; 13 | 14 | mod util; 15 | 16 | pub use dataset::DataSetReader; 17 | pub use stateful::decode::{DynStatefulDecoder, StatefulDecode, StatefulDecoder}; 18 | pub use stateful::encode::StatefulEncoder; 19 | -------------------------------------------------------------------------------- /parser/src/stateful/mod.rs: -------------------------------------------------------------------------------- 1 | //! Stateful counterparts for decoding and encoding DICOM content. 2 | 3 | pub mod decode; 4 | pub mod encode; 5 | -------------------------------------------------------------------------------- /parser/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Seek}; 2 | 3 | pub trait ReadSeek: Read + Seek {} 4 | impl ReadSeek for T where T: Read + Seek {} 5 | 6 | /// Obtain an iterator of `n` void elements. 7 | /// Useful for doing something N times as efficiently as possible. 8 | pub fn n_times(n: usize) -> VoidRepeatN { 9 | VoidRepeatN { i: n } 10 | } 11 | 12 | pub struct VoidRepeatN { 13 | i: usize, 14 | } 15 | 16 | impl Iterator for VoidRepeatN { 17 | type Item = (); 18 | 19 | fn next(&mut self) -> Option<()> { 20 | match self.i { 21 | 0 => None, 22 | _ => { 23 | self.i -= 1; 24 | Some(()) 25 | } 26 | } 27 | } 28 | 29 | fn size_hint(&self) -> (usize, Option) { 30 | (self.i, Some(self.i)) 31 | } 32 | } 33 | 34 | impl ExactSizeIterator for VoidRepeatN { 35 | fn len(&self) -> usize { 36 | self.i 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::n_times; 43 | 44 | #[test] 45 | fn void_repeat_n() { 46 | let it = n_times(5); 47 | assert_eq!(it.len(), 5); 48 | let mut k = 0; 49 | for v in it { 50 | assert_eq!(v, ()); 51 | k += 1; 52 | } 53 | assert_eq!(k, 5); 54 | let mut it = n_times(0); 55 | assert_eq!(it.len(), 0); 56 | assert_eq!(it.next(), None); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pixeldata/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-pixeldata" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho ", "Peter Evers "] 5 | edition = "2018" 6 | rust-version = "1.72.0" 7 | license = "MIT OR Apache-2.0" 8 | description = "A high-level API for decoding DICOM objects into images and ndarrays" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | categories = ["multimedia::images"] 11 | keywords = ["dicom"] 12 | readme = "README.md" 13 | 14 | [[bin]] 15 | name = "dicom-transcode" 16 | path = "src/bin/dicom-transcode.rs" 17 | required-features = ["cli"] 18 | 19 | [dependencies] 20 | dicom-object = { path = "../object", version = "0.8.1" } 21 | dicom-core = { path = "../core", version = "0.8.1" } 22 | dicom-encoding = { path = "../encoding", version = "0.8.1" } 23 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry", version = "0.8.1" } 24 | dicom-dictionary-std = { path = "../dictionary-std", version = "0.8.0" } 25 | snafu = "0.8" 26 | byteorder = "1.4.3" 27 | gdcm-rs = { version = "0.6", optional = true } 28 | rayon = { version = "1.5.0", optional = true } 29 | ndarray = { version = "0.15.1", optional = true } 30 | num-traits = "0.2.12" 31 | tracing = "0.1.34" 32 | 33 | [dependencies.image] 34 | version = "0.25.1" 35 | default-features = false 36 | features = ["jpeg", "png", "pnm", "tiff", "webp", "bmp", "exr"] 37 | optional = true 38 | 39 | [dependencies.clap] 40 | version = "4.4.2" 41 | optional = true 42 | features = ["cargo", "derive"] 43 | 44 | [dependencies.tracing-subscriber] 45 | version = "0.3.17" 46 | optional = true 47 | 48 | [dev-dependencies] 49 | rstest = "0.25" 50 | dicom-test-files = "0.3" 51 | 52 | [features] 53 | default = ["rayon", "native"] 54 | 55 | ndarray = ["dep:ndarray"] 56 | image = ["dep:image"] 57 | 58 | # Rust native image codec implementations 59 | native = ["dicom-transfer-syntax-registry/native", "jpeg", "rle"] 60 | # native JPEG codec implementation 61 | jpeg = ["dicom-transfer-syntax-registry/jpeg"] 62 | # native JPEG XL codec implementation 63 | jpegxl = ["dicom-transfer-syntax-registry/jpegxl"] 64 | # native RLE lossless codec implementation 65 | rle = ["dicom-transfer-syntax-registry/rle"] 66 | # JPEG 2000 decoding via OpenJPEG static linking 67 | openjpeg-sys = ["dicom-transfer-syntax-registry/openjpeg-sys"] 68 | # JPEG 2000 decoding via Rust port of OpenJPEG 69 | openjp2 = ["dicom-transfer-syntax-registry/openjp2"] 70 | # JpegLS via CharLS 71 | charls = ["dicom-transfer-syntax-registry/charls"] 72 | # use vcpkg to build CharLS 73 | charls-vcpkg = ["dicom-transfer-syntax-registry/charls-vcpkg"] 74 | 75 | # replace pixel data decoding to use GDCM 76 | gdcm = ["gdcm-rs"] 77 | # use Rayon for image decoding 78 | rayon = ["dep:rayon", "image?/rayon", "dicom-transfer-syntax-registry/rayon"] 79 | 80 | # enable command line tools 81 | cli = ["dep:clap", "dep:tracing-subscriber"] 82 | 83 | [package.metadata.docs.rs] 84 | features = ["image", "ndarray"] 85 | -------------------------------------------------------------------------------- /pixeldata/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `pixeldata` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-pixeldata.svg)](https://crates.io/crates/dicom-pixeldata) 4 | [![Documentation](https://docs.rs/dicom-pixeldata/badge.svg)](https://docs.rs/dicom-pixeldata) 5 | 6 | This sub-project is directed at users of the DICOM-rs ecosystem. 7 | It provides constructs for handling DICOM pixel data 8 | and is responsible for decoding pixel data elements 9 | into images or multi-dimensional arrays. 10 | 11 | This crate is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 12 | 13 | ## Binary 14 | 15 | `dicom-pixeldata` also offers the `dicom-transcode` command-line tool 16 | (enable Cargo feature `cli`). 17 | You can use it to transcode a DICOM file to another transfer syntax, 18 | transforming pixel data along the way. 19 | 20 | ```none 21 | Transcode a DICOM file 22 | 23 | Usage: dicom-transcode [OPTIONS] <--ts |--expl-vr-le|--impl-vr-le|--jpeg-baseline|--jpeg-ls-lossless|--jpeg-ls|--jpeg-xl-lossless|--jpeg-xl> 24 | 25 | Arguments: 26 | 27 | 28 | Options: 29 | -o, --output The output file (default is to change the extension to .new.dcm) 30 | --quality The encoding quality (from 0 to 100) 31 | --effort The encoding effort (from 0 to 100) 32 | --ts Transcode to the Transfer Syntax indicated by UID 33 | --expl-vr-le Transcode to Explicit VR Little Endian 34 | --impl-vr-le Transcode to Implicit VR Little Endian 35 | --jpeg-baseline Transcode to JPEG baseline (8-bit) 36 | --jpeg-ls-lossless Transcode to JPEG-LS lossless 37 | --jpeg-ls Transcode to JPEG-LS near-lossless 38 | --jpeg-xl-lossless Transcode to JPEG XL lossless 39 | --jpeg-xl Transcode to JPEG XL 40 | --retain-implementation Retain the original implementation class UID and version name 41 | -v, --verbose Verbose mode 42 | -h, --help Print help 43 | -V, --version Print version 44 | ``` 45 | -------------------------------------------------------------------------------- /pixeldata/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | -------------------------------------------------------------------------------- /pixeldata/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-pixeldata-fuzz" 3 | version = "0.0.0" 4 | description = "Fuzz testing for the dicom-pixeldata crate" 5 | authors = [] 6 | publish = false 7 | edition = "2021" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | byteorder = "1.4.3" 15 | 16 | [dependencies.dicom-core] 17 | path = "../../core" 18 | 19 | [dependencies.dicom-dictionary-std] 20 | path = "../../dictionary-std" 21 | 22 | [dependencies.dicom-object] 23 | path = "../../object" 24 | 25 | [dependencies.dicom-pixeldata] 26 | path = ".." 27 | 28 | # Prevent this from interfering with workspaces 29 | [workspace] 30 | members = ["."] 31 | 32 | [[bin]] 33 | name = "decode_simple_image" 34 | path = "fuzz_targets/decode_simple_image.rs" 35 | test = false 36 | doc = false 37 | 38 | [[bin]] 39 | name = "decode_image_file" 40 | path = "fuzz_targets/decode_image_file.rs" 41 | test = false 42 | doc = false 43 | 44 | [profile.release] 45 | debug = true 46 | -------------------------------------------------------------------------------- /pixeldata/fuzz/fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -eu 3 | # pick fuzz target binary 4 | FUZZ_TARGET="${1-decode_image_file}" 5 | env ASAN_OPTIONS=allocator_may_return_null=1:max_allocation_size_mb=1024 cargo +nightly fuzz run "$FUZZ_TARGET" -- -max_len=16777216 6 | -------------------------------------------------------------------------------- /pixeldata/fuzz/fuzz_targets/decode_image_file.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use dicom_pixeldata::PixelDecoder; 3 | use libfuzzer_sys::fuzz_target; 4 | use std::error::Error; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | let _ = fuzz(data); 8 | }); 9 | 10 | fn fuzz(data: &[u8]) -> Result<(), Box> { 11 | // deserialize random bytes 12 | let obj = dicom_object::from_reader(data)?; 13 | 14 | // decode them as an image 15 | let decoded = obj.decode_pixel_data()?; 16 | 17 | // turn into native pixel data vector 18 | let pixels: Vec = decoded.to_vec()?; 19 | 20 | // assert that the vector length matches the expected number of samples 21 | let size = decoded.rows() as u64 22 | * decoded.columns() as u64 23 | * decoded.samples_per_pixel() as u64 24 | * decoded.number_of_frames() as u64; 25 | 26 | assert_eq!(pixels.len() as u64, size); 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /pixeldata/fuzz/fuzz_targets/decode_simple_image.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use byteorder::{LittleEndian as LE, ReadBytesExt}; 3 | use dicom_core::{DataElement, PrimitiveValue, VR}; 4 | use dicom_dictionary_std::tags; 5 | use dicom_object::{FileDicomObject, FileMetaTableBuilder, InMemDicomObject}; 6 | use dicom_pixeldata::PixelDecoder; 7 | use libfuzzer_sys::fuzz_target; 8 | use std::{error::Error, io::Read}; 9 | 10 | fuzz_target!(|data: &[u8]| { 11 | let _ = fuzz(data); 12 | }); 13 | 14 | /// Obtain a simple DICOM file with an image from reading some raw bytes 15 | /// in a non-standard format. 16 | /// This simplifies the fuzz test payload to 17 | /// obtain coverage over pixel data decoding sooner. 18 | /// 19 | /// For the time being, 20 | /// it always returns native pixel data objects in Explicit VR Little Endian. 21 | fn build_simple_image(data: &[u8]) -> Result, Box> { 22 | let mut obj = InMemDicomObject::new_empty(); 23 | 24 | let reader = &mut (&data[..]); 25 | 26 | let rows = reader.read_u16::()?; 27 | let cols = reader.read_u16::()?; 28 | let spp: u16 = if reader.read_u8()? < 0x80 { 1 } else { 3 }; 29 | 30 | obj.put(DataElement::new( 31 | tags::ROWS, 32 | VR::US, 33 | PrimitiveValue::from(rows), 34 | )); 35 | 36 | obj.put(DataElement::new( 37 | tags::COLUMNS, 38 | VR::US, 39 | PrimitiveValue::from(cols), 40 | )); 41 | 42 | obj.put(DataElement::new( 43 | tags::SAMPLES_PER_PIXEL, 44 | VR::US, 45 | PrimitiveValue::from(spp), 46 | )); 47 | 48 | if spp > 1 { 49 | obj.put_element(DataElement::new( 50 | tags::PLANAR_CONFIGURATION, 51 | VR::US, 52 | PrimitiveValue::from(0), 53 | )); 54 | } 55 | 56 | let pi = if spp == 3 { "RGB" } else { "MONOCHROME2" }; 57 | obj.put(DataElement::new( 58 | tags::PHOTOMETRIC_INTERPRETATION, 59 | VR::CS, 60 | PrimitiveValue::from(pi), 61 | )); 62 | 63 | let bits_allocated = if reader.read_u8()? < 0x80 { 8 } else { 16 }; 64 | let bits_stored = bits_allocated; 65 | let high_bit = bits_stored - 1; 66 | 67 | obj.put(DataElement::new( 68 | tags::BITS_ALLOCATED, 69 | VR::US, 70 | PrimitiveValue::from(bits_allocated), 71 | )); 72 | obj.put(DataElement::new( 73 | tags::BITS_STORED, 74 | VR::US, 75 | PrimitiveValue::from(bits_stored), 76 | )); 77 | obj.put(DataElement::new( 78 | tags::HIGH_BIT, 79 | VR::US, 80 | PrimitiveValue::from(high_bit), 81 | )); 82 | let pixel_representation = reader.read_u8()? >> 7; 83 | obj.put(DataElement::new( 84 | tags::PIXEL_REPRESENTATION, 85 | VR::US, 86 | PrimitiveValue::from(pixel_representation), 87 | )); 88 | 89 | if spp == 1 { 90 | let rescale_intercept = reader.read_f64::()?; 91 | let rescale_slope = reader.read_f64::()?; 92 | 93 | obj.put(DataElement::new( 94 | tags::RESCALE_INTERCEPT, 95 | VR::DS, 96 | PrimitiveValue::from(rescale_intercept.to_string()), 97 | )); 98 | obj.put(DataElement::new( 99 | tags::RESCALE_SLOPE, 100 | VR::DS, 101 | PrimitiveValue::from(rescale_slope.to_string()), 102 | )); 103 | } 104 | 105 | obj.put(DataElement::new( 106 | tags::VOILUT_FUNCTION, 107 | VR::CS, 108 | PrimitiveValue::from("IDENTITY"), 109 | )); 110 | 111 | // finally, grab some pixel data 112 | 113 | let size = rows as u64 * cols as u64 * spp as u64 * (bits_allocated / 8) as u64; 114 | 115 | let mut pixeldata = vec![]; 116 | Read::take(reader, size).read_to_end(&mut pixeldata)?; 117 | 118 | obj.put(DataElement::new( 119 | tags::PIXEL_DATA, 120 | if bits_allocated == 16 { VR::OW } else { VR::OB }, 121 | PrimitiveValue::from(pixeldata), 122 | )); 123 | 124 | let meta = FileMetaTableBuilder::new() 125 | .media_storage_sop_class_uid("1.2.840.10008.5.1.4.1.1.7") 126 | .media_storage_sop_instance_uid("2.25.221743183549175336412959299516406387775") 127 | .transfer_syntax("1.2.840.10008.1.2.1"); 128 | 129 | Ok(obj.with_meta(meta)?) 130 | } 131 | 132 | fn fuzz(data: &[u8]) -> Result<(), Box> { 133 | // deserialize random bytes 134 | let obj = build_simple_image(data)?; 135 | 136 | // decode them as an image 137 | let decoded = obj.decode_pixel_data()?; 138 | 139 | // turn into native pixel data vector 140 | let pixels: Vec = decoded.to_vec()?; 141 | 142 | // assert that the vector length matches the expected number of samples 143 | let size = 144 | decoded.rows() as u64 * decoded.columns() as u64 * decoded.samples_per_pixel() as u64; 145 | 146 | assert_eq!(pixels.len() as u64, size); 147 | 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /pixeldata/src/encapsulation.rs: -------------------------------------------------------------------------------- 1 | //! DICOM Pixel encapsulation 2 | //! 3 | //! This module implements encapsulation for pixel data. 4 | use dicom_core::value::fragments::Fragments; 5 | use dicom_core::value::Value; 6 | use std::vec; 7 | 8 | /// Encapsulate the pixel data of a list of frames. 9 | /// 10 | /// Check [Fragments] in case more control over the processing is required. 11 | /// 12 | /// # Example 13 | /// ``` 14 | /// use dicom_core::DataElement; 15 | /// use dicom_core::VR::OB; 16 | /// use dicom_dictionary_std::tags; 17 | /// use dicom_object::InMemDicomObject; 18 | /// use dicom_pixeldata::encapsulation::encapsulate; 19 | /// 20 | /// // Frames are represented as Vec> 21 | /// // Single 512x512 frame 22 | /// let frames = vec![vec![0; 262144]]; 23 | /// let pixel_data = encapsulate(frames); 24 | /// let element = DataElement::new(tags::PIXEL_DATA, OB, pixel_data); 25 | /// ``` 26 | pub fn encapsulate(frames: Vec>) -> Value { 27 | let fragments = frames 28 | .into_iter() 29 | .map(|frame| Fragments::new(frame, 0)) 30 | .collect::>(); 31 | 32 | Value::PixelSequence(fragments.into()) 33 | } 34 | 35 | /// Encapsulate the pixel data of a single frame. If `fragment_size` is zero then `frame.len()` will 36 | /// be used instead. 37 | /// 38 | /// # Example 39 | /// ``` 40 | /// use dicom_core::DataElement; 41 | /// use dicom_core::VR::OB; 42 | /// use dicom_dictionary_std::tags; 43 | /// use dicom_object::InMemDicomObject; 44 | /// use dicom_pixeldata::encapsulation::encapsulate_single_frame; 45 | /// 46 | /// // Frames are represented as Vec> 47 | /// // Single 512x512 frame 48 | /// let frame = vec![0; 262144]; 49 | /// let pixel_data = encapsulate_single_frame(frame, 0); 50 | /// let element = DataElement::new(tags::PIXEL_DATA, OB, pixel_data); 51 | /// ``` 52 | pub fn encapsulate_single_frame(frame: Vec, fragment_size: u32) -> Value { 53 | let fragments = vec![Fragments::new(frame, fragment_size)]; 54 | 55 | Value::PixelSequence(fragments.into()) 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | 62 | #[test] 63 | fn test_encapsulate() { 64 | if let Value::PixelSequence(enc) = encapsulate(vec![vec![20, 30, 40], vec![50, 60, 70, 80]]) 65 | { 66 | let offset_table = enc.offset_table(); 67 | let fragments = enc.fragments(); 68 | assert_eq!(offset_table.len(), 2); 69 | assert_eq!(fragments.len(), 2); 70 | assert_eq!(fragments[0].len(), 4); 71 | assert_eq!(fragments[1].len(), 4); 72 | } else { 73 | unreachable!("encapsulate should always return a PixelSequence"); 74 | } 75 | } 76 | 77 | #[test] 78 | fn test_encapsulate_single_framme() { 79 | if let Value::PixelSequence(enc) = encapsulate_single_frame(vec![20, 30, 40], 1) { 80 | let offset_table = enc.offset_table(); 81 | let fragments = enc.fragments(); 82 | assert_eq!(offset_table.len(), 1); 83 | assert_eq!(fragments.len(), 2); 84 | assert_eq!(fragments[0].len(), 2); 85 | assert_eq!(fragments[1].len(), 2); 86 | } else { 87 | unreachable!("encapsulate should always return a PixelSequence"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pixeldata/tests/image_reader.rs: -------------------------------------------------------------------------------- 1 | //! Test module for pixel data readers (adapters). 2 | //! 3 | //! This test suite deliberately avoids the use of the decoded pixel data API 4 | //! for a greater focus on covering 5 | //! pixel data reading and writing capabilities 6 | //! from transfer syntax implementations. 7 | 8 | #[cfg(feature = "native")] 9 | mod jpeg { 10 | 11 | // always compare with an error margin, 12 | // since JPEG baseline is lossy 13 | const L1_ERROR_THRESHOLD: u32 = 8; 14 | fn pixel_value_matches(got: [u8; 3], expected: [u8; 3]) { 15 | let error = got[0].abs_diff(expected[0]) as u32 16 | + got[1].abs_diff(expected[1]) as u32 17 | + got[2].abs_diff(expected[2]) as u32; 18 | 19 | assert!( 20 | error <= L1_ERROR_THRESHOLD, 21 | "pixel values {:?} does not match with expected {:?}", 22 | got, 23 | expected 24 | ); 25 | } 26 | 27 | use std::convert::TryInto; 28 | 29 | use dicom_encoding::adapters::DecodeError; 30 | use dicom_encoding::adapters::PixelDataObject; 31 | use dicom_encoding::transfer_syntax::Codec; 32 | use dicom_encoding::transfer_syntax::TransferSyntaxIndex; 33 | use dicom_object::open_file; 34 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 35 | 36 | #[test] 37 | fn can_read_jpeg() { 38 | let file_path = dicom_test_files::path("pydicom/color3d_jpeg_baseline.dcm").unwrap(); 39 | let obj = open_file(file_path).unwrap(); 40 | 41 | let ts_jpeg = TransferSyntaxRegistry 42 | .get("1.2.840.10008.1.2.4.50") 43 | .expect("Transfer syntax _JPEG Baseline_ should be registered for this test"); 44 | 45 | // can fetch reader 46 | 47 | let Codec::EncapsulatedPixelData(Some(img_reader), _img_writer) = ts_jpeg.codec() else { 48 | panic!("Missing image reader in _JPEG Baseline_ transfer syntax impl"); 49 | }; 50 | 51 | // can read one frame 52 | let mut dst = Vec::new(); 53 | img_reader 54 | .decode_frame(&obj, 0, &mut dst) 55 | .expect("Failed to decode frame"); 56 | 57 | let columns = obj.cols().expect("DICOM object should have Columns"); 58 | assert_eq!(columns, 640, "Unexpected image width"); 59 | let rows = obj.rows().expect("DICOM object should have Columns"); 60 | assert_eq!(rows, 480, "Unexpected image height"); 61 | let columns = columns as usize; 62 | let rows = rows as usize; 63 | 64 | // curated expected pixel data values at the given X,Y coordinates 65 | // in frame 0 66 | let pixel_gt = [ 67 | // pixel at (0,0): (1,1,3) 68 | ((0, 0), [1, 1, 3]), 69 | // and so on 70 | ((15, 19), [81, 115, 163]), 71 | ((70, 33), [118, 118, 118]), 72 | ((380, 39), [74, 166, 125]), 73 | // begin ultrasound region 74 | ((313, 190), [104, 104, 104]), 75 | ((350, 256), [122, 122, 122]), 76 | // end ultrasound region 77 | ((512, 460), [28, 35, 45]), 78 | ]; 79 | 80 | for ((x, y), expected) in pixel_gt { 81 | let i = (y * columns + x) * 3; 82 | pixel_value_matches(dst[i..i + 3].try_into().unwrap(), expected); 83 | } 84 | let frame_size = columns * rows * 3; 85 | assert_eq!(dst.len(), frame_size); 86 | 87 | // decode a different frame, keeping the previous one in memory 88 | img_reader 89 | .decode_frame(&obj, 63, &mut dst) 90 | .expect("Failed to decode frame"); 91 | 92 | // curated expected pixel data values at the given X,Y coordinates 93 | // in frame 63 94 | let pixel_gt = [ 95 | ((0, 0), [1, 1, 3]), 96 | ((15, 19), [81, 115, 163]), 97 | ((70, 33), [118, 118, 118]), 98 | ((380, 39), [74, 166, 125]), 99 | // only the ultrasound region changes 100 | ((313, 190), [84, 84, 84]), 101 | ((350, 256), [6, 6, 6]), 102 | // end ultrasound region 103 | ((512, 460), [28, 35, 45]), 104 | ]; 105 | for ((x, y), expected) in pixel_gt { 106 | let i = frame_size + (y * columns + x) * 3; 107 | pixel_value_matches(dst[i..i + 3].try_into().unwrap(), expected); 108 | } 109 | assert_eq!(dst.len(), frame_size * 2); 110 | 111 | // cannot read out of bounds 112 | assert!(matches!( 113 | img_reader.decode_frame(&obj, 120, &mut dst), 114 | Err(DecodeError::FrameRangeOutOfBounds), 115 | )); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /scpproxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-scpproxy" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho ", "Paul Knopf "] 5 | description = "A proxy SCP server, for logging and diagnostics" 6 | edition = "2018" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/Enet4/dicom-rs" 9 | categories = ["command-line-utilities"] 10 | keywords = ["dicom"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | clap = { version = "4.0.18", features = ["cargo"] } 15 | dicom-ul = { path = "../ul/", version = "0.8.1" } 16 | snafu = "0.8" 17 | tracing = "0.1.34" 18 | tracing-subscriber = "0.3.11" 19 | bytes = "1.6" 20 | -------------------------------------------------------------------------------- /scpproxy/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `scpproxy` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-scpproxy.svg)](https://crates.io/crates/dicom-scpproxy) 4 | [![Documentation](https://docs.rs/dicom-scpproxy/badge.svg)](https://docs.rs/dicom-scpproxy) 5 | 6 | This is an implementation of the Proxy SCP, which can be used for logging and debugging purposes. 7 | 8 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 9 | 10 | ## Usage 11 | 12 | ``` 13 | scpproxy [OPTIONS] 14 | 15 | FLAGS: 16 | -h, --help Prints help information 17 | -V, --version Prints version information 18 | 19 | OPTIONS: 20 | -l, --listen-port The port that we will listen for SCU connections on [default: 3333] 21 | 22 | ARGS: 23 | The destination host name (SCP) 24 | The destination host port (SCP) 25 | ``` 26 | -------------------------------------------------------------------------------- /storescp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-storescp" 3 | version = "0.8.1" 4 | authors = ["Victor Saase ", "Eduardo Pinho "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/Enet4/dicom-rs" 8 | description = "A server accepting DICOM C-STORE" 9 | categories = ["command-line-utilities"] 10 | keywords = ["dicom", "store"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | clap = { version = "4.0.18", features = ["derive"] } 15 | dicom-core = { path = '../core', version = "0.8.1" } 16 | dicom-ul = { path = '../ul', version = "0.8.1", features = ["async"] } 17 | dicom-object = { path = '../object', version = "0.8.1" } 18 | dicom-encoding = { path = "../encoding/", version = "0.8.1" } 19 | dicom-dictionary-std = { path = "../dictionary-std/", version = "0.8.0" } 20 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.8.1" } 21 | snafu = "0.8" 22 | tracing = "0.1.36" 23 | tracing-subscriber = "0.3.15" 24 | tokio = { version = "1.38.0", features = ["full"] } 25 | 26 | -------------------------------------------------------------------------------- /storescp/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `storescp` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-storescp.svg)](https://crates.io/crates/dicom-storescp) 4 | 5 | This is an implementation of the DICOM Storage SCP (C-STORE), 6 | which can be used for receiving DICOM files from other DICOM devices. 7 | 8 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 9 | 10 | ## Usage 11 | 12 | ```none 13 | dicom-storescp [-p tcp_port] [-o dicom_storage_dir] [OPTIONS] 14 | ``` 15 | 16 | Note that this tool is not necessarily a drop-in replacement 17 | for `storescp` tools in other DICOM software projects. 18 | Run `dicom-storescp --help` for more details. 19 | -------------------------------------------------------------------------------- /storescp/src/transfer.rs: -------------------------------------------------------------------------------- 1 | //! Accepted storage transfer options 2 | 3 | use dicom_dictionary_std::uids::*; 4 | 5 | /// A list of supported abstract syntaxes for storage services 6 | #[allow(deprecated)] 7 | pub static ABSTRACT_SYNTAXES: &[&str] = &[ 8 | CT_IMAGE_STORAGE, 9 | ENHANCED_CT_IMAGE_STORAGE, 10 | STANDALONE_CURVE_STORAGE, 11 | STANDALONE_OVERLAY_STORAGE, 12 | SECONDARY_CAPTURE_IMAGE_STORAGE, 13 | ULTRASOUND_IMAGE_STORAGE_RETIRED, 14 | NUCLEAR_MEDICINE_IMAGE_STORAGE_RETIRED, 15 | MR_IMAGE_STORAGE, 16 | ENHANCED_MR_IMAGE_STORAGE, 17 | MR_SPECTROSCOPY_STORAGE, 18 | ENHANCED_MR_COLOR_IMAGE_STORAGE, 19 | ULTRASOUND_MULTI_FRAME_IMAGE_STORAGE_RETIRED, 20 | COMPUTED_RADIOGRAPHY_IMAGE_STORAGE, 21 | DIGITAL_X_RAY_IMAGE_STORAGE_FOR_PRESENTATION, 22 | DIGITAL_X_RAY_IMAGE_STORAGE_FOR_PROCESSING, 23 | ENCAPSULATED_PDF_STORAGE, 24 | ENCAPSULATED_CDA_STORAGE, 25 | ENCAPSULATED_STL_STORAGE, 26 | GRAYSCALE_SOFTCOPY_PRESENTATION_STATE_STORAGE, 27 | POSITRON_EMISSION_TOMOGRAPHY_IMAGE_STORAGE, 28 | BREAST_TOMOSYNTHESIS_IMAGE_STORAGE, 29 | BREAST_PROJECTION_X_RAY_IMAGE_STORAGE_FOR_PRESENTATION, 30 | BREAST_PROJECTION_X_RAY_IMAGE_STORAGE_FOR_PROCESSING, 31 | ENHANCED_PET_IMAGE_STORAGE, 32 | RT_IMAGE_STORAGE, 33 | NUCLEAR_MEDICINE_IMAGE_STORAGE, 34 | ULTRASOUND_MULTI_FRAME_IMAGE_STORAGE, 35 | MULTI_FRAME_SINGLE_BIT_SECONDARY_CAPTURE_IMAGE_STORAGE, 36 | MULTI_FRAME_GRAYSCALE_BYTE_SECONDARY_CAPTURE_IMAGE_STORAGE, 37 | MULTI_FRAME_GRAYSCALE_WORD_SECONDARY_CAPTURE_IMAGE_STORAGE, 38 | MULTI_FRAME_TRUE_COLOR_SECONDARY_CAPTURE_IMAGE_STORAGE, 39 | BASIC_TEXT_SR_STORAGE, 40 | ENHANCED_SR_STORAGE, 41 | COMPREHENSIVE_SR_STORAGE, 42 | VERIFICATION, 43 | ]; 44 | -------------------------------------------------------------------------------- /storescu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-storescu" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/Enet4/dicom-rs" 8 | description = "A DICOM C-ECHO command line interface" 9 | categories = ["command-line-utilities"] 10 | keywords = ["dicom"] 11 | readme = "README.md" 12 | 13 | [features] 14 | default = ["transcode"] 15 | # support DICOM transcoding 16 | transcode = ["dep:dicom-pixeldata"] 17 | 18 | [dependencies] 19 | clap = { version = "4.0.18", features = ["derive"] } 20 | dicom-core = { path = '../core', version = "0.8.1" } 21 | dicom-dictionary-std = { path = "../dictionary-std/", version = "0.8.0" } 22 | dicom-encoding = { path = "../encoding/", version = "0.8.1" } 23 | dicom-object = { path = '../object', version = "0.8.1" } 24 | dicom-pixeldata = { version = "0.8.1", path = "../pixeldata", optional = true } 25 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.8.1" } 26 | dicom-ul = { path = '../ul', version = "0.8.1", features = ["async"] } 27 | walkdir = "2.3.2" 28 | indicatif = "0.17.0" 29 | tracing = "0.1.34" 30 | tracing-subscriber = "0.3.11" 31 | snafu = "0.8" 32 | 33 | [dependencies.tokio] 34 | version = "1.38.0" 35 | features = ["rt", "rt-multi-thread", "macros", "sync"] 36 | -------------------------------------------------------------------------------- /storescu/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `storescu` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-storescu.svg)](https://crates.io/crates/dicom-storescu) 4 | [![Documentation](https://docs.rs/dicom-storescu/badge.svg)](https://docs.rs/dicom-storescu) 5 | 6 | This is an implementation of the DICOM Storage SCU (C-STORE), 7 | which can be used for uploading DICOM files to other DICOM devices. 8 | 9 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 10 | 11 | ## Usage 12 | 13 | Note that this tool is not necessarily a drop-in replacement 14 | for `storescu` tools in other DICOM software projects. 15 | 16 | ```none 17 | DICOM C-STORE SCU 18 | 19 | USAGE: 20 | dicom-storescu [FLAGS] [OPTIONS] [files]... 21 | 22 | FLAGS: 23 | --fail-first fail if not all DICOM files can be transferred 24 | -h, --help Prints help information 25 | -V, --version Prints version information 26 | -v, --verbose verbose mode 27 | 28 | OPTIONS: 29 | --called-ae-title 30 | the called Application Entity title, overrides AE title in address if present [default: ANY-SCP] 31 | 32 | --calling-ae-title the calling Application Entity title [default: STORE-SCU] 33 | --max-pdu-length the maximum PDU length accepted by the SCU [default: 16384] 34 | -m, --message-id the C-STORE message ID [default: 1] 35 | --username user identity username 36 | --password user identity password 37 | --kerberos-service-ticket user identity Kerberos service ticket 38 | --saml-assertion user identity SAML assertion 39 | --jwt user identity JWT 40 | 41 | ARGS: 42 | socket address to Store SCP, optionally with AE title (example: "STORE-SCP@127.0.0.1:104") 43 | ... the DICOM file(s) to store 44 | ``` 45 | 46 | Example: 47 | 48 | ```sh 49 | dicom-storescu MAIN-STORAGE@192.168.1.99:104 xray1.dcm xray2.dcm 50 | ``` 51 | -------------------------------------------------------------------------------- /storescu/out.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Enet4/dicom-rs/f840a24f87337350c6522df91478d6ab5e763d76/storescu/out.json -------------------------------------------------------------------------------- /toimage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-toimage" 3 | version = "0.8.0" 4 | edition = "2018" 5 | rust-version = "1.72.0" 6 | authors = ["Eduardo Pinho "] 7 | description = "A CLI tool for converting DICOM files into general purpose image files" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | categories = ["command-line-utilities"] 11 | keywords = ["cli", "dicom", "image", "image-conversion"] 12 | readme = "README.md" 13 | 14 | [features] 15 | default = ['dicom-object/inventory-registry', 'dicom-pixeldata/native', 'dicom-pixeldata/jpegxl'] 16 | 17 | [dependencies] 18 | clap = { version = "4.0.18", features = ["derive"] } 19 | dicom-core = { path = "../core", version = "0.8.1" } 20 | dicom-dictionary-std = { version = "0.8.0", path = "../dictionary-std" } 21 | dicom-object = { path = "../object/", version = "0.8.1" } 22 | dicom-pixeldata = { path = "../pixeldata/", version = "0.8.1", default-features = false, features = ["image", "rayon"] } 23 | snafu = "0.8" 24 | tracing = "0.1.34" 25 | tracing-subscriber = "0.3.11" 26 | -------------------------------------------------------------------------------- /toimage/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `toimage` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-toimage.svg)](https://crates.io/crates/dicom-toimage) 4 | [![Documentation](https://docs.rs/dicom-toimage/badge.svg)](https://docs.rs/dicom-toimage) 5 | 6 | A command line utility for converting DICOM image files 7 | into general purpose image files (e.g. PNG). 8 | 9 | This tool is part of the [DICOM-rs](https://github.com/Enet4/dicom-rs) project. 10 | 11 | ## Usage 12 | 13 | ```none 14 | dicom-toimage [OPTIONS] ... 15 | 16 | Arguments: 17 | ... Paths to the DICOM files (or directories) to convert 18 | 19 | Options: 20 | -r, --recursive Parse directory recursively 21 | -o, --out Path to the output image, including file extension (replaces input extension with `.png` by default) 22 | -d, --outdir Path to the output directory in bulk conversion mode, conflicts with `output` 23 | -e, --ext Extension when converting multiple files (default is to replace input extension with `.png`) 24 | -F, --frame Frame number (0-indexed) [default: 0] 25 | --8bit Force output bit depth to 8 bits per sample 26 | --16bit Force output bit depth to 16 bits per sample 27 | --unwrap Output the raw pixel data instead of decoding it 28 | --fail-first Stop on the first failed conversion 29 | -v, --verbose Print more information about the image and the output file 30 | -h, --help Print help 31 | -V, --version Print version 32 | ``` 33 | -------------------------------------------------------------------------------- /transfer-syntax-registry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-transfer-syntax-registry" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho "] 5 | description = "A registry of DICOM transfer syntaxes" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | keywords = ["dicom"] 11 | readme = "README.md" 12 | 13 | [features] 14 | default = ["rayon", "simd"] 15 | 16 | # inventory for compile time plugin-based transfer syntax registration 17 | inventory-registry = ['dicom-encoding/inventory-registry'] 18 | 19 | # natively implemented image encodings 20 | native = ["jpeg", "rle"] 21 | # native implementations that work on Windows 22 | native_windows = ["jpeg", "rle"] 23 | # native JPEG support 24 | jpeg = ["jpeg-decoder", "jpeg-encoder"] 25 | # native JPEG XL support 26 | jpegxl = ["dep:jxl-oxide", "dep:zune-jpegxl", "dep:zune-core"] 27 | 28 | # JPEG 2000 support via the OpenJPEG Rust port, 29 | # works on Linux and a few other platforms 30 | openjp2 = ["dep:jpeg2k", "jpeg2k/openjp2"] 31 | # native RLE lossless support 32 | rle = [] 33 | # enable Rayon for JPEG decoding 34 | rayon = ["jpeg-decoder?/rayon", "jxl-oxide?/rayon"] 35 | # enable SIMD operations for JPEG encoding 36 | simd = ["jpeg-encoder?/simd"] 37 | 38 | # JPEG 2000 support via the OpenJPEG native bindings, 39 | # conflicts with `openjp2` 40 | openjpeg-sys = ["dep:jpeg2k", "jpeg2k/openjpeg-sys"] 41 | 42 | # jpeg LS support via charls bindings 43 | charls = ["dep:charls"] 44 | 45 | # use vcpkg to build CharLS 46 | charls-vcpkg = ["charls?/vcpkg"] 47 | 48 | # build OpenJPEG with multithreading, 49 | # implies "rayon" 50 | openjpeg-sys-threads = ["rayon", "jpeg2k?/threads"] 51 | 52 | # multithreading for JPEG XL encoding 53 | zune-jpegxl-threads = ["zune-jpegxl?/threads"] 54 | 55 | [dependencies] 56 | dicom-core = { path = "../core", version = "0.8.1" } 57 | dicom-encoding = { path = "../encoding", version = "0.8.1" } 58 | lazy_static = "1.2.0" 59 | byteordered = "0.6" 60 | tracing = "0.1.34" 61 | 62 | [dependencies.jpeg2k] 63 | version = "0.9.1" 64 | optional = true 65 | default-features = false 66 | 67 | [dependencies.jpeg-decoder] 68 | version = "0.3.0" 69 | optional = true 70 | 71 | [dependencies.jpeg-encoder] 72 | version = "0.6" 73 | optional = true 74 | 75 | [dependencies.charls] 76 | version = "0.4.2" 77 | optional = true 78 | features = ["static"] 79 | 80 | [dependencies.jxl-oxide] 81 | version = "0.10.2" 82 | optional = true 83 | 84 | [dependencies.zune-jpegxl] 85 | version = "0.4.0" 86 | optional = true 87 | default-features = false 88 | features = ["std"] 89 | 90 | [dependencies.zune-core] 91 | version = "0.4.12" 92 | optional = true 93 | default-features = false 94 | 95 | [package.metadata.docs.rs] 96 | features = ["native"] 97 | 98 | [dev-dependencies] 99 | dicom-test-files = "0.3" 100 | -------------------------------------------------------------------------------- /transfer-syntax-registry/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `transfer-syntax-registry` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-transfer-syntax-registry.svg)](https://crates.io/crates/dicom-transfer-syntax-registry) 4 | [![Documentation](https://docs.rs/dicom-transfer-syntax-registry/badge.svg)](https://docs.rs/dicom-transfer-syntax-registry) 5 | 6 | This sub-project implements a registry of DICOM transfer syntaxes, 7 | which can be optionally extended. 8 | 9 | An implementation based on [`inventory`] can be used through the Cargo feature 10 | `inventory-registry`. `inventory` allows for users to register new transfer 11 | syntax implementations in a compile time plugin-like fashion, 12 | but not all environments support it (such as WebAssembly). 13 | 14 | [`inventory`]: https://crates.io/crates/inventory 15 | -------------------------------------------------------------------------------- /transfer-syntax-registry/src/adapters/jpeg2k.rs: -------------------------------------------------------------------------------- 1 | //! Support for JPEG 2000 image decoding. 2 | 3 | use dicom_encoding::adapters::{decode_error, DecodeResult, PixelDataObject, PixelDataReader}; 4 | use dicom_encoding::snafu::prelude::*; 5 | use jpeg2k::Image; 6 | use std::borrow::Cow; 7 | use tracing::warn; 8 | 9 | // Check jpeg2k backend conflicts 10 | #[cfg(all(feature = "openjp2", feature = "openjpeg-sys"))] 11 | compile_error!( 12 | "feature \"openjp2\" and feature \"openjpeg-sys\" cannot be enabled at the same time" 13 | ); 14 | 15 | /// Pixel data adapter for transfer syntaxes based on JPEG 2000. 16 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 17 | pub struct Jpeg2000Adapter; 18 | 19 | impl PixelDataReader for Jpeg2000Adapter { 20 | /// Decode a single frame in JPEG 2000 from a DICOM object. 21 | fn decode_frame( 22 | &self, 23 | src: &dyn PixelDataObject, 24 | frame: u32, 25 | dst: &mut Vec, 26 | ) -> DecodeResult<()> { 27 | let cols = src 28 | .cols() 29 | .context(decode_error::MissingAttributeSnafu { name: "Columns" })?; 30 | let rows = src 31 | .rows() 32 | .context(decode_error::MissingAttributeSnafu { name: "Rows" })?; 33 | let samples_per_pixel = 34 | src.samples_per_pixel() 35 | .context(decode_error::MissingAttributeSnafu { 36 | name: "SamplesPerPixel", 37 | })?; 38 | let bits_allocated = src 39 | .bits_allocated() 40 | .context(decode_error::MissingAttributeSnafu { 41 | name: "BitsAllocated", 42 | })?; 43 | 44 | ensure_whatever!( 45 | bits_allocated == 8 || bits_allocated == 16, 46 | "BitsAllocated other than 8 or 16 is not supported" 47 | ); 48 | 49 | let nr_frames = src.number_of_frames().unwrap_or(1) as usize; 50 | 51 | ensure!( 52 | nr_frames > frame as usize, 53 | decode_error::FrameRangeOutOfBoundsSnafu 54 | ); 55 | 56 | let bytes_per_sample = bits_allocated / 8; 57 | 58 | // `stride` it the total number of bytes for each sample plane 59 | let stride: usize = bytes_per_sample as usize * cols as usize * rows as usize; 60 | dst.reserve_exact(samples_per_pixel as usize * stride); 61 | let base_offset = dst.len(); 62 | dst.resize(base_offset + (samples_per_pixel as usize * stride), 0); 63 | 64 | let raw = src 65 | .raw_pixel_data() 66 | .whatever_context("Expected to have raw pixel data available")?; 67 | 68 | let frame_data = if raw.fragments.len() == 1 || raw.fragments.len() == nr_frames { 69 | // assuming 1:1 frame-to-fragment mapping 70 | Cow::Borrowed( 71 | raw.fragments 72 | .get(frame as usize) 73 | .with_whatever_context(|| { 74 | format!("Missing fragment #{} for the frame requested", frame) 75 | })?, 76 | ) 77 | } else { 78 | // Some embedded JPEGs might span multiple fragments. 79 | // In this case we look up the basic offset table 80 | // and gather all of the frame's fragments in a single vector. 81 | // Note: not the most efficient way to do this, 82 | // consider optimizing later with byte chunk readers 83 | let base_offset = raw.offset_table.get(frame as usize).copied(); 84 | let base_offset = if frame == 0 { 85 | base_offset.unwrap_or(0) as usize 86 | } else { 87 | base_offset 88 | .with_whatever_context(|| format!("Missing offset for frame #{}", frame))? 89 | as usize 90 | }; 91 | let next_offset = raw.offset_table.get(frame as usize + 1); 92 | 93 | let mut offset = 0; 94 | let mut fragments = Vec::new(); 95 | for fragment in &raw.fragments { 96 | // include it 97 | if offset >= base_offset { 98 | fragments.extend_from_slice(fragment); 99 | } 100 | offset += fragment.len() + 8; 101 | if let Some(&next_offset) = next_offset { 102 | if offset >= next_offset as usize { 103 | // next fragment is for the next frame 104 | break; 105 | } 106 | } 107 | } 108 | 109 | Cow::Owned(fragments) 110 | }; 111 | 112 | let image = Image::from_bytes(&frame_data).whatever_context("jpeg2k decoder failure")?; 113 | 114 | // Note: we cannot use `get_pixels` 115 | // because the current implementation narrows the data 116 | // down to 8 bits per sample 117 | let components = image.components(); 118 | 119 | // write each component into the destination buffer 120 | for (component_i, component) in components.iter().enumerate() { 121 | if component_i > samples_per_pixel as usize { 122 | warn!( 123 | "JPEG 2000 image has more components than expected ({} > {})", 124 | component_i, samples_per_pixel 125 | ); 126 | break; 127 | } 128 | 129 | // write in standard layout 130 | for (i, sample) in component.data().iter().enumerate() { 131 | let offset = base_offset 132 | + i * samples_per_pixel as usize * bytes_per_sample as usize 133 | + component_i * bytes_per_sample as usize; 134 | dst[offset..offset + bytes_per_sample as usize] 135 | .copy_from_slice(&sample.to_le_bytes()[..bytes_per_sample as usize]); 136 | } 137 | } 138 | 139 | Ok(()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /transfer-syntax-registry/src/adapters/mod.rs: -------------------------------------------------------------------------------- 1 | //! Root module for extended pixel data adapters. 2 | //! 3 | //! Additional support for certain transfer syntaxes 4 | //! can be added via Cargo features. 5 | //! 6 | //! - [`jpeg`](jpeg) provides native JPEG decoding 7 | //! (baseline and lossless) 8 | //! and encoding (baseline). 9 | //! Requires the `jpeg` feature, 10 | //! enabled by default. 11 | //! - [`jpeg2k`](jpeg2k) contains JPEG 2000 support, 12 | //! which is currently available through [OpenJPEG]. 13 | //! The `openjp2` feature provides native JPEG 2000 decoding 14 | //! via the [Rust port of OpenJPEG][OpenJPEG-rs], 15 | //! which works on Linux and Mac OS, but not on Windows. 16 | //! Alternatively, enable the `openjpeg-sys` feature 17 | //! to statically link to the OpenJPEG reference implementation. 18 | //! `openjp2` is enabled by the feature `native`. 19 | //! To build on Windows, enable `native_windows` instead. 20 | //! - [`jpegxl`](jpegxl) provides JPEG XL decoding and encoding, 21 | //! through `jxl-oxide` and `zune-jpegxl`, respectively. 22 | //! - [`rle_lossless`](rle_lossless) provides native RLE lossless decoding. 23 | //! Requires the `rle` feature, 24 | //! enabled by default. 25 | //! 26 | //! [OpenJPEG]: https://github.com/uclouvain/openjpeg 27 | //! [OpenJPEG-rs]: https://crates.io/crates/openjp2 28 | #[cfg(feature = "jpeg")] 29 | pub mod jpeg; 30 | #[cfg(any(feature = "openjp2", feature = "openjpeg-sys"))] 31 | pub mod jpeg2k; 32 | #[cfg(feature = "charls")] 33 | pub mod jpegls; 34 | #[cfg(feature = "jpegxl")] 35 | pub mod jpegxl; 36 | #[cfg(feature = "rle")] 37 | pub mod rle_lossless; 38 | 39 | pub mod uncompressed; 40 | 41 | /// **Note:** This module is a stub. 42 | /// Enable the `jpeg` feature to use this module. 43 | #[cfg(not(feature = "jpeg"))] 44 | pub mod jpeg {} 45 | 46 | /// **Note:** This module is a stub. 47 | /// Enable either `openjp2` or `openjpeg-sys` to use this module. 48 | #[cfg(not(any(feature = "openjp2", feature = "openjpeg-sys")))] 49 | pub mod jpeg2k {} 50 | 51 | /// **Note:** This module is a stub. 52 | /// Enable the `rle` feature to use this module. 53 | #[cfg(not(feature = "rle"))] 54 | pub mod rle {} 55 | 56 | /// **Note:** This module is a stub. 57 | /// Enable the `charls` feature to use this module. 58 | #[cfg(not(feature = "charls"))] 59 | pub mod jpegls {} 60 | 61 | /// **Note:** This module is a stub. 62 | /// Enable the `jpegxl` feature to use this module. 63 | #[cfg(not(feature = "jpegxl"))] 64 | pub mod jpegxl {} 65 | -------------------------------------------------------------------------------- /transfer-syntax-registry/src/adapters/uncompressed.rs: -------------------------------------------------------------------------------- 1 | //! Support for encapsulated uncompressed via pixel data adapter. 2 | 3 | use dicom_core::{ 4 | ops::{AttributeAction, AttributeOp}, 5 | PrimitiveValue, Tag, 6 | }; 7 | use dicom_encoding::{ 8 | adapters::{ 9 | decode_error, encode_error, DecodeResult, EncodeOptions, EncodeResult, PixelDataObject, 10 | PixelDataReader, PixelDataWriter, 11 | }, 12 | snafu::OptionExt, 13 | }; 14 | 15 | /// Adapter for [Encapsulated Uncompressed Explicit VR Little Endian][1] 16 | /// [1]: https://dicom.nema.org/medical/dicom/2023c/output/chtml/part05/sect_A.4.11.html 17 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 18 | pub struct UncompressedAdapter; 19 | 20 | impl PixelDataReader for UncompressedAdapter { 21 | fn decode(&self, src: &dyn PixelDataObject, dst: &mut Vec) -> DecodeResult<()> { 22 | // just flatten all fragments into the output vector 23 | let pixeldata = src 24 | .raw_pixel_data() 25 | .context(decode_error::MissingAttributeSnafu { name: "Pixel Data" })?; 26 | 27 | for fragment in pixeldata.fragments { 28 | dst.extend_from_slice(&fragment); 29 | } 30 | 31 | Ok(()) 32 | } 33 | 34 | fn decode_frame( 35 | &self, 36 | src: &dyn PixelDataObject, 37 | frame: u32, 38 | dst: &mut Vec, 39 | ) -> DecodeResult<()> { 40 | // just copy the specific fragment into the output vector 41 | let pixeldata = src 42 | .raw_pixel_data() 43 | .context(decode_error::MissingAttributeSnafu { name: "Pixel Data" })?; 44 | 45 | let fragment = pixeldata 46 | .fragments 47 | .get(frame as usize) 48 | .context(decode_error::FrameRangeOutOfBoundsSnafu)?; 49 | 50 | dst.extend_from_slice(fragment); 51 | 52 | Ok(()) 53 | } 54 | } 55 | 56 | impl PixelDataWriter for UncompressedAdapter { 57 | fn encode_frame( 58 | &self, 59 | src: &dyn PixelDataObject, 60 | frame: u32, 61 | _options: EncodeOptions, 62 | dst: &mut Vec, 63 | ) -> EncodeResult> { 64 | let cols = src 65 | .cols() 66 | .context(encode_error::MissingAttributeSnafu { name: "Columns" })?; 67 | let rows = src 68 | .rows() 69 | .context(encode_error::MissingAttributeSnafu { name: "Rows" })?; 70 | let samples_per_pixel = 71 | src.samples_per_pixel() 72 | .context(encode_error::MissingAttributeSnafu { 73 | name: "SamplesPerPixel", 74 | })?; 75 | let bits_allocated = src 76 | .bits_allocated() 77 | .context(encode_error::MissingAttributeSnafu { 78 | name: "BitsAllocated", 79 | })?; 80 | 81 | let bytes_per_sample = (bits_allocated / 8) as usize; 82 | let frame_size = 83 | cols as usize * rows as usize * samples_per_pixel as usize * bytes_per_sample; 84 | 85 | // identify frame data using the frame index 86 | let pixeldata_uncompressed = &src 87 | .raw_pixel_data() 88 | .context(encode_error::MissingAttributeSnafu { name: "Pixel Data" })? 89 | .fragments[0]; 90 | 91 | let len_before = pixeldata_uncompressed.len(); 92 | 93 | let frame_data = pixeldata_uncompressed 94 | .get(frame_size * frame as usize..frame_size * (frame as usize + 1)) 95 | .whatever_context("Frame index out of bounds")?; 96 | 97 | // Copy the the data to the output 98 | dst.extend_from_slice(frame_data); 99 | 100 | // provide attribute changes 101 | Ok(vec![ 102 | // Encapsulated Pixel Data Value Total Length 103 | AttributeOp::new( 104 | Tag(0x7FE0, 0x0003), 105 | AttributeAction::Set(PrimitiveValue::from(len_before as u64)), 106 | ), 107 | ]) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /transfer-syntax-registry/src/deflate.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of Deflated Explicit VR Little Endian. 2 | use std::io::{Read, Write}; 3 | 4 | use byteordered::Endianness; 5 | use dicom_encoding::{Codec, TransferSyntax, transfer_syntax::DataRWAdapter}; 6 | use flate2; 7 | use flate2::Compression; 8 | 9 | /// Immaterial type representing an adapter for deflated data. 10 | #[derive(Debug)] 11 | pub struct FlateAdapter; 12 | 13 | /// **Fully implemented**: Deflated Explicit VR Little Endian 14 | pub const DEFLATED_EXPLICIT_VR_LITTLE_ENDIAN: TransferSyntax = TransferSyntax::new( 15 | "1.2.840.10008.1.2.1.99", 16 | "Deflated Explicit VR Little Endian", 17 | Endianness::Little, 18 | true, 19 | Codec::Dataset(FlateAdapter), 20 | ); 21 | 22 | impl DataRWAdapter for FlateAdapter 23 | where 24 | R: Read, 25 | W: Write, 26 | { 27 | type Reader = flate2::read::DeflateDecoder; 28 | type Writer = flate2::write::DeflateEncoder; 29 | 30 | fn adapt_reader(&self, reader: R) -> Self::Reader 31 | where 32 | R: Read, 33 | { 34 | flate2::read::DeflateDecoder::new(reader) 35 | } 36 | 37 | fn adapt_writer(&self, writer: W) -> Self::Writer 38 | where 39 | W: Write, 40 | { 41 | flate2::write::DeflateEncoder::new(writer, Compression::fast()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/adapters/mod.rs: -------------------------------------------------------------------------------- 1 | //! Utility module for testing pixel data adapters. 2 | use std::borrow::Cow; 3 | 4 | use dicom_core::value::{InMemFragment, PixelFragmentSequence}; 5 | use dicom_encoding::adapters::{PixelDataObject, RawPixelData}; 6 | 7 | /// A test data object. 8 | /// 9 | /// Can be used to test pixel data adapters 10 | /// without having to open a real DICOM file using `dicom_object`. 11 | #[derive(Debug)] 12 | pub(crate) struct TestDataObject { 13 | pub ts_uid: String, 14 | pub rows: u16, 15 | pub columns: u16, 16 | pub bits_allocated: u16, 17 | pub bits_stored: u16, 18 | pub samples_per_pixel: u16, 19 | pub photometric_interpretation: &'static str, 20 | pub number_of_frames: u32, 21 | pub flat_pixel_data: Option>, 22 | pub pixel_data_sequence: Option>, 23 | } 24 | 25 | impl PixelDataObject for TestDataObject { 26 | fn transfer_syntax_uid(&self) -> &str { 27 | &self.ts_uid 28 | } 29 | 30 | fn rows(&self) -> Option { 31 | Some(self.rows) 32 | } 33 | 34 | fn cols(&self) -> Option { 35 | Some(self.columns) 36 | } 37 | 38 | fn samples_per_pixel(&self) -> Option { 39 | Some(self.samples_per_pixel) 40 | } 41 | 42 | fn bits_allocated(&self) -> Option { 43 | Some(self.bits_allocated) 44 | } 45 | 46 | fn bits_stored(&self) -> Option { 47 | Some(self.bits_stored) 48 | } 49 | 50 | fn photometric_interpretation(&self) -> Option<&str> { 51 | Some(&self.photometric_interpretation) 52 | } 53 | 54 | fn number_of_frames(&self) -> Option { 55 | Some(self.number_of_frames) 56 | } 57 | 58 | fn number_of_fragments(&self) -> Option { 59 | match &self.pixel_data_sequence { 60 | Some(v) => Some(v.fragments().len() as u32), 61 | _ => None, 62 | } 63 | } 64 | 65 | fn fragment(&self, fragment: usize) -> Option> { 66 | match (&self.flat_pixel_data, &self.pixel_data_sequence) { 67 | (Some(_), Some(_)) => { 68 | panic!("Invalid pixel data object (both flat and fragment sequence)") 69 | } 70 | (_, Some(v)) => v 71 | .fragments() 72 | .get(fragment) 73 | .map(|f| Cow::Borrowed(f.as_slice())), 74 | (Some(v), _) => { 75 | if fragment == 0 { 76 | Some(Cow::Borrowed(v)) 77 | } else { 78 | None 79 | } 80 | } 81 | (None, None) => None, 82 | } 83 | } 84 | 85 | fn offset_table(&self) -> Option> { 86 | match &self.pixel_data_sequence { 87 | Some(v) => Some(Cow::Borrowed(v.offset_table())), 88 | _ => None, 89 | } 90 | } 91 | 92 | fn raw_pixel_data(&self) -> Option { 93 | match (&self.flat_pixel_data, &self.pixel_data_sequence) { 94 | (Some(_), Some(_)) => { 95 | panic!("Invalid pixel data object (both flat and fragment sequence)") 96 | } 97 | (Some(v), _) => Some(RawPixelData { 98 | fragments: vec![v.clone()].into(), 99 | offset_table: Default::default(), 100 | }), 101 | (_, Some(v)) => Some(RawPixelData { 102 | fragments: v.fragments().into(), 103 | offset_table: v.offset_table().into(), 104 | }), 105 | _ => None, 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/base.rs: -------------------------------------------------------------------------------- 1 | //! Registry tests, to ensure that transfer syntaxes are properly 2 | //! registered when linked together in a separate program. 3 | 4 | use dicom_encoding::transfer_syntax::TransferSyntaxIndex; 5 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 6 | 7 | fn assert_fully_supported(registry: T, mut uid: &'static str, name: &'static str) 8 | where 9 | T: TransferSyntaxIndex, 10 | { 11 | let ts = registry.get(uid); 12 | assert!(ts.is_some(), "Registry did not provide TS {}", uid); 13 | let ts = ts.unwrap(); 14 | if uid.ends_with("\0") { 15 | uid = &uid[0..uid.len() - 1]; 16 | } 17 | assert_eq!(ts.uid(), uid); 18 | assert_eq!(ts.name(), name); 19 | assert!(ts.is_fully_supported()); 20 | } 21 | 22 | #[test] 23 | fn contains_base_ts() { 24 | let registry = TransferSyntaxRegistry; 25 | 26 | // contains implicit VR little endian and is fully supported 27 | assert_fully_supported(registry, "1.2.840.10008.1.2", "Implicit VR Little Endian"); 28 | 29 | // should work the same for trailing null characters 30 | assert_fully_supported(registry, "1.2.840.10008.1.2\0", "Implicit VR Little Endian"); 31 | 32 | // contains explicit VR little endian and is fully supported 33 | assert_fully_supported(registry, "1.2.840.10008.1.2.1", "Explicit VR Little Endian"); 34 | 35 | // contains explicit VR big endian and is fully supported 36 | assert_fully_supported(registry, "1.2.840.10008.1.2.2", "Explicit VR Big Endian"); 37 | } 38 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/rle.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for RLE lossless pixel data reading and writing 2 | #![cfg(feature = "rle")] 3 | 4 | mod adapters; 5 | 6 | use std::{ 7 | fs::File, 8 | io::{Read, Seek, SeekFrom}, 9 | path::Path, 10 | }; 11 | 12 | use adapters::TestDataObject; 13 | use dicom_core::value::PixelFragmentSequence; 14 | use dicom_encoding::{adapters::PixelDataReader, Codec}; 15 | use dicom_transfer_syntax_registry::entries::RLE_LOSSLESS; 16 | 17 | fn read_data_piece(test_file: impl AsRef, offset: u64, length: usize) -> Vec { 18 | let mut file = File::open(test_file).unwrap(); 19 | let mut buf = vec![0; length]; 20 | file.seek(SeekFrom::Start(offset)).unwrap(); 21 | file.read_exact(&mut buf).unwrap(); 22 | buf 23 | } 24 | 25 | fn check_u16_rgb_pixel(pixels: &[u8], columns: u16, x: u16, y: u16, expected_pixel: [u16; 3]) { 26 | let i = (y as usize * columns as usize * 3 + x as usize * 3) * 2; 27 | let got = [ 28 | u16::from_le_bytes([pixels[i], pixels[i + 1]]), 29 | u16::from_le_bytes([pixels[i + 2], pixels[i + 3]]), 30 | u16::from_le_bytes([pixels[i + 4], pixels[i + 5]]), 31 | ]; 32 | assert_eq!( 33 | got, expected_pixel, 34 | "pixel sample mismatch at ({}, {}): {:?} vs {:?}", 35 | x, y, got, expected_pixel 36 | ); 37 | } 38 | 39 | fn check_i16_monochrome_pixel(pixels: &[u8], columns: u16, x: u16, y: u16, expected: i16) { 40 | let i = (y as usize * columns as usize + x as usize) * 2; 41 | let got = i16::from_le_bytes([pixels[i], pixels[i + 1]]); 42 | assert_eq!( 43 | got, expected, 44 | "pixel sample mismatch at ({}, {}): {:?} vs {:?}", 45 | x, y, got, expected 46 | ); 47 | } 48 | 49 | #[test] 50 | fn read_rle_1() { 51 | let test_file = dicom_test_files::path("WG04/RLE/CT1_RLE").unwrap(); 52 | 53 | // manually fetch the pixel data fragment from the file: 54 | 55 | // PixelData offset: 0x18f6 56 | // first fragment item offset: 0x1902 57 | // first fragment size: 4 58 | // second fragment item offset: 0x190e 59 | // second fragment size: 248330 60 | 61 | // single fragment found in file data offset 0x1916, 248330 bytes 62 | let buf = read_data_piece(test_file, 0x1916, 248330); 63 | 64 | // create test object 65 | let obj = TestDataObject { 66 | // RLE lossless 67 | ts_uid: "1.2.840.10008.1.2.5".to_string(), 68 | rows: 512, 69 | columns: 512, 70 | bits_allocated: 16, 71 | bits_stored: 16, 72 | samples_per_pixel: 1, 73 | photometric_interpretation: "MONOCHROME2", 74 | number_of_frames: 1, 75 | flat_pixel_data: None, 76 | pixel_data_sequence: Some(PixelFragmentSequence::new(vec![], vec![buf])), 77 | }; 78 | 79 | // instantiate RLE lossless adapter 80 | 81 | let Codec::EncapsulatedPixelData(Some(adapter), _) = RLE_LOSSLESS.codec() else { 82 | panic!("RLE lossless pixel data reader not found") 83 | }; 84 | 85 | let mut dest = vec![]; 86 | 87 | // decode the whole image (1 frame) 88 | 89 | adapter 90 | .decode(&obj, &mut dest) 91 | .expect("RLE frame decoding failed"); 92 | 93 | // inspect the result 94 | assert_eq!(dest.len(), 512 * 512 * 2); 95 | 96 | // check a few known pixels 97 | check_i16_monochrome_pixel(&dest, 512, 16, 16, -2_000); 98 | 99 | check_i16_monochrome_pixel(&dest, 512, 255, 255, 980); 100 | 101 | check_i16_monochrome_pixel(&dest, 512, 342, 336, 188); 102 | 103 | // decode a single frame 104 | let mut dest2 = vec![]; 105 | adapter 106 | .decode_frame(&obj, 0, &mut dest2) 107 | .expect("RLE frame decoding failed"); 108 | 109 | // the outcome should be the same 110 | assert_eq!(dest, dest2); 111 | } 112 | 113 | #[test] 114 | fn read_rle_2() { 115 | let test_file = dicom_test_files::path("pydicom/SC_rgb_rle_16bit.dcm").unwrap(); 116 | 117 | // manually fetch the pixel data fragment from the file: 118 | // PixelData offset: 0x51a 119 | // first fragment item offset: 0x526 120 | // first fragment size: 0 121 | // second fragment item offset: 0x52e 122 | // second fragment size: 1264 123 | 124 | // single fragment found in file data offset 0x536, 1264 bytes 125 | let buf = read_data_piece(test_file, 0x536, 1_264); 126 | 127 | // create test object 128 | let obj = TestDataObject { 129 | // RLE lossless 130 | ts_uid: "1.2.840.10008.1.2.5".to_string(), 131 | rows: 100, 132 | columns: 100, 133 | bits_allocated: 16, 134 | bits_stored: 16, 135 | samples_per_pixel: 3, 136 | photometric_interpretation: "RGB", 137 | number_of_frames: 1, 138 | flat_pixel_data: None, 139 | pixel_data_sequence: Some(PixelFragmentSequence::new(vec![], vec![buf])), 140 | }; 141 | 142 | // instantiate RLE lossless adapter 143 | 144 | let Codec::EncapsulatedPixelData(Some(adapter), _) = RLE_LOSSLESS.codec() else { 145 | panic!("RLE lossless pixel data reader not found") 146 | }; 147 | 148 | let mut dest = vec![]; 149 | 150 | // decode the whole image (1 frame) 151 | 152 | adapter 153 | .decode(&obj, &mut dest) 154 | .expect("RLE frame decoding failed"); 155 | 156 | // inspect the result 157 | assert_eq!(dest.len(), 100 * 100 * 2 * 3); 158 | 159 | // check a few known pixels 160 | check_u16_rgb_pixel(&dest, 100, 0, 0, [0xFFFF, 0, 0]); 161 | 162 | check_u16_rgb_pixel(&dest, 100, 99, 19, [0xFFFF, 32_896, 32_896]); 163 | 164 | check_u16_rgb_pixel(&dest, 100, 54, 65, [0, 0, 0]); 165 | 166 | check_u16_rgb_pixel(&dest, 100, 10, 95, [0xFFFF, 0xFFFF, 0xFFFF]); 167 | } 168 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/submit_dataset.rs: -------------------------------------------------------------------------------- 1 | //! Independent test for submission of a dummy TS implementation 2 | //! with a dummy data set adapter. 3 | //! 4 | //! Only applicable to the inventory-based registry. 5 | #![cfg(feature = "inventory-registry")] 6 | 7 | use dicom_encoding::{ 8 | submit_transfer_syntax, Codec, DataRWAdapter, Endianness, NeverPixelAdapter, TransferSyntax, 9 | TransferSyntaxIndex, 10 | }; 11 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 12 | use std::io::{Read, Write}; 13 | 14 | /// this would, in theory, provide a dataset adapter 15 | #[derive(Debug)] 16 | struct DummyCodecAdapter; 17 | 18 | impl DataRWAdapter for DummyCodecAdapter { 19 | type Reader = Box; 20 | type Writer = Box; 21 | 22 | fn adapt_reader(&self, reader: R) -> Self::Reader 23 | where 24 | R: Read, 25 | { 26 | Box::new(reader) as Box<_> 27 | } 28 | 29 | fn adapt_writer(&self, writer: W) -> Self::Writer 30 | where 31 | W: Write, 32 | { 33 | Box::new(writer) as Box<_> 34 | } 35 | } 36 | 37 | // install this dummy as a private transfer syntax 38 | submit_transfer_syntax! { 39 | TransferSyntax::new( 40 | "1.2.840.10008.9999.9999.1", 41 | "Dummy Explicit VR Little Endian", 42 | Endianness::Little, 43 | true, 44 | Codec::Dataset::<_, NeverPixelAdapter, NeverPixelAdapter>(Some(DummyCodecAdapter)) 45 | ) 46 | } 47 | 48 | #[test] 49 | fn contains_dummy_ts() { 50 | // contains our dummy TS, and claims to be fully supported 51 | let ts = TransferSyntaxRegistry.get("1.2.840.10008.9999.9999.1"); 52 | assert!(ts.is_some()); 53 | let ts = ts.unwrap(); 54 | assert_eq!(ts.uid(), "1.2.840.10008.9999.9999.1"); 55 | assert_eq!(ts.name(), "Dummy Explicit VR Little Endian"); 56 | assert!(ts.is_fully_supported()); 57 | assert!(ts.can_decode_dataset()); 58 | assert!(ts.can_decode_all()); 59 | } 60 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/submit_pixel.rs: -------------------------------------------------------------------------------- 1 | //! Independent test for submission of a dummy TS implementation 2 | //! with a pixel data adapter. 3 | //! 4 | //! Only applicable to the inventory-based registry. 5 | #![cfg(feature = "inventory-registry")] 6 | 7 | use dicom_encoding::{ 8 | adapters::{ 9 | DecodeResult, EncodeOptions, EncodeResult, PixelDataObject, PixelDataReader, 10 | PixelDataWriter, 11 | }, 12 | submit_transfer_syntax, Codec, NeverAdapter, TransferSyntax, TransferSyntaxIndex, 13 | }; 14 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 15 | 16 | /// this would, in theory, provide a pixel data adapter 17 | #[derive(Debug)] 18 | struct DummyPixelAdapter; 19 | 20 | impl PixelDataReader for DummyPixelAdapter { 21 | fn decode(&self, _src: &dyn PixelDataObject, _dst: &mut Vec) -> DecodeResult<()> { 22 | panic!("Stub, not supposed to be called") 23 | } 24 | 25 | fn decode_frame( 26 | &self, 27 | _src: &dyn PixelDataObject, 28 | _frame: u32, 29 | _dst: &mut Vec, 30 | ) -> DecodeResult<()> { 31 | panic!("Stub, not supposed to be called") 32 | } 33 | } 34 | 35 | impl PixelDataWriter for DummyPixelAdapter { 36 | fn encode( 37 | &self, 38 | _src: &dyn PixelDataObject, 39 | _options: EncodeOptions, 40 | _dst: &mut Vec>, 41 | _offset_table: &mut Vec, 42 | ) -> EncodeResult> { 43 | panic!("Stub, not supposed to be called") 44 | } 45 | 46 | fn encode_frame( 47 | &self, 48 | _src: &dyn PixelDataObject, 49 | _frame: u32, 50 | _options: EncodeOptions, 51 | _dst: &mut Vec, 52 | ) -> EncodeResult> { 53 | panic!("Stub, not supposed to be called") 54 | } 55 | } 56 | 57 | // install this dummy as a private transfer syntax 58 | submit_transfer_syntax! { 59 | TransferSyntax::::new_ele( 60 | "1.2.840.10008.9999.9999.2", 61 | "Dummy Lossless", 62 | Codec::EncapsulatedPixelData(Some(DummyPixelAdapter), Some(DummyPixelAdapter)) 63 | ) 64 | } 65 | 66 | #[test] 67 | fn contains_dummy_ts() { 68 | // contains our dummy TS, and claims to be fully supported 69 | let ts = TransferSyntaxRegistry.get("1.2.840.10008.9999.9999.2"); 70 | assert!(ts.is_some()); 71 | let ts = ts.unwrap(); 72 | assert_eq!(ts.uid(), "1.2.840.10008.9999.9999.2"); 73 | assert_eq!(ts.name(), "Dummy Lossless"); 74 | assert!(!ts.is_codec_free()); 75 | assert!(ts.is_fully_supported()); 76 | assert!(ts.can_decode_dataset()); 77 | assert!(ts.can_decode_all()); 78 | } 79 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/submit_pixel_stub.rs: -------------------------------------------------------------------------------- 1 | //! Independent test for submission of a dummy TS implementation 2 | //! without adapters. 3 | //! 4 | //! Only applicable to the inventory-based registry. 5 | #![cfg(feature = "inventory-registry")] 6 | 7 | use dicom_encoding::{submit_ele_transfer_syntax, Codec, TransferSyntaxIndex}; 8 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 9 | 10 | // install this dummy as a private transfer syntax 11 | submit_ele_transfer_syntax!( 12 | "1.2.840.10008.1.999.9999.99999", 13 | "Dummy Little Endian", 14 | Codec::EncapsulatedPixelData(None, None) 15 | ); 16 | 17 | const ALWAYS_DUMMY: &str = "1.2.840.10008.1.999.9999.99999.2"; 18 | 19 | // install more dummy as a private transfer syntax 20 | submit_ele_transfer_syntax!( 21 | ALWAYS_DUMMY, 22 | "Always Dummy Lossless Little Endian", 23 | Codec::EncapsulatedPixelData(None, None) 24 | ); 25 | 26 | const FOREVER_DUMMY: &str = "1.2.840.10008.1.999.9999.99999.3"; 27 | const FOREVER_DUMMY_NAME: &str = "Forever Dummy Hierarchical Little Endian"; 28 | 29 | // install event more dummy as a private transfer syntax 30 | submit_ele_transfer_syntax!( 31 | FOREVER_DUMMY, 32 | FOREVER_DUMMY_NAME, 33 | Codec::EncapsulatedPixelData(None, None) 34 | ); 35 | 36 | #[test] 37 | fn contains_stub_ts() { 38 | // contains our stub TS, not fully fully supported, 39 | // but enough to read some datasets 40 | let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.999.9999.99999"); 41 | assert!(ts.is_some()); 42 | let ts = ts.unwrap(); 43 | assert_eq!(ts.uid(), "1.2.840.10008.1.999.9999.99999"); 44 | assert_eq!(ts.name(), "Dummy Little Endian"); 45 | assert!(!ts.is_fully_supported()); 46 | assert!(ts.is_unsupported_pixel_encapsulation()); 47 | // can obtain a data set decoder 48 | assert!(ts.decoder().is_some()); 49 | 50 | let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.999.9999.99999.2"); 51 | assert!(ts.is_some()); 52 | let ts = ts.unwrap(); 53 | assert_eq!(ts.name(), "Always Dummy Lossless Little Endian"); 54 | let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.999.9999.99999.3"); 55 | assert!(ts.is_some()); 56 | let ts = ts.unwrap(); 57 | assert_eq!(ts.name(), "Forever Dummy Hierarchical Little Endian"); 58 | } 59 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/submit_replace.rs: -------------------------------------------------------------------------------- 1 | //! Independent test for submission of a dummy TS implementation 2 | //! to replace a built-in stub. 3 | //! 4 | //! Only applicable to the inventory-based registry. 5 | #![cfg(feature = "inventory-registry")] 6 | 7 | use dicom_encoding::{ 8 | submit_transfer_syntax, Codec, DataRWAdapter, Endianness, NeverPixelAdapter, TransferSyntax, 9 | TransferSyntaxIndex, 10 | }; 11 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 12 | use std::io::{Read, Write}; 13 | 14 | /// this would, in theory, provide a dataset adapter 15 | #[derive(Debug)] 16 | struct DummyCodecAdapter; 17 | 18 | impl DataRWAdapter for DummyCodecAdapter { 19 | type Reader = Box; 20 | type Writer = Box; 21 | 22 | fn adapt_reader(&self, _reader: R) -> Self::Reader 23 | where 24 | R: Read, 25 | { 26 | unimplemented!() 27 | } 28 | 29 | fn adapt_writer(&self, _writer: W) -> Self::Writer 30 | where 31 | W: Write, 32 | { 33 | unimplemented!() 34 | } 35 | } 36 | 37 | // pretend to implement JPIP Referenced Deflate, 38 | // which is in the registry by default, 39 | // but not fully supported 40 | submit_transfer_syntax! { 41 | TransferSyntax::new( 42 | "1.2.840.10008.1.2.4.95", 43 | "JPIP Referenced Deflate (Override)", 44 | Endianness::Little, 45 | true, 46 | Codec::Dataset::<_, NeverPixelAdapter, NeverPixelAdapter>(Some(DummyCodecAdapter)) 47 | ) 48 | } 49 | 50 | #[test] 51 | fn contains_dummy_ts() { 52 | // contains our dummy TS, and claims to be fully supported 53 | let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2.4.95"); 54 | assert!(ts.is_some()); 55 | let ts = ts.unwrap(); 56 | assert_eq!(ts.uid(), "1.2.840.10008.1.2.4.95"); 57 | assert_eq!(ts.name(), "JPIP Referenced Deflate (Override)"); 58 | assert!(ts.is_fully_supported()); 59 | assert!(ts.can_decode_dataset()); 60 | assert!(ts.can_decode_all()); 61 | } 62 | -------------------------------------------------------------------------------- /transfer-syntax-registry/tests/submit_replace_precondition.rs: -------------------------------------------------------------------------------- 1 | //! Independent test for the precondition of `submit_replace`: 2 | //! the transfer syntax used must be stubbed. 3 | //! 4 | //! Only applicable to the inventory-based registry. 5 | #![cfg(feature = "inventory-registry")] 6 | 7 | use dicom_encoding::{Codec, TransferSyntaxIndex}; 8 | use dicom_transfer_syntax_registry::TransferSyntaxRegistry; 9 | 10 | /// Assert that this transfer syntax is provided built-in as a stub. 11 | /// 12 | /// If this changes, please replace the transfer syntax to test against 13 | /// and override. 14 | #[test] 15 | fn registry_has_stub_ts_by_default() { 16 | // this TS is provided by default, but not fully supported 17 | let ts = TransferSyntaxRegistry.get("1.2.840.10008.1.2.4.95"); 18 | assert!(ts.is_some()); 19 | let ts = ts.unwrap(); 20 | assert_eq!(ts.uid(), "1.2.840.10008.1.2.4.95"); 21 | assert_eq!(ts.name(), "JPIP Referenced Deflate"); 22 | assert!(matches!( 23 | ts.codec(), 24 | Codec::Dataset(None) | Codec::EncapsulatedPixelData(None, None) 25 | )); 26 | 27 | assert_eq!(ts.can_decode_dataset(), false); 28 | assert_eq!(ts.can_decode_all(), false); 29 | } 30 | -------------------------------------------------------------------------------- /ul/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-ul" 3 | version = "0.8.1" 4 | authors = ["Eduardo Pinho ", "Paul Knopf "] 5 | description = "Types and methods for interacting with the DICOM Upper Layer Protocol" 6 | edition = "2018" 7 | rust-version = "1.72.0" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/Enet4/dicom-rs" 10 | categories = ["network-programming"] 11 | keywords = ["dicom", "network"] 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | byteordered = "0.6" 16 | bytes = "^1.6" 17 | dicom-encoding = { path = "../encoding/", version = "0.8.1" } 18 | dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry/", version = "0.8.1", default-features = false } 19 | snafu = "0.8" 20 | tracing = "0.1.34" 21 | 22 | [dependencies.tokio] 23 | version = "^1.38" 24 | optional = true 25 | features = [ 26 | "rt", 27 | "rt-multi-thread", 28 | "net", 29 | "io-util", 30 | "time" 31 | ] 32 | 33 | [dev-dependencies] 34 | dicom-dictionary-std = { path = "../dictionary-std" } 35 | matches = "0.1.8" 36 | rstest = "0.25" 37 | tokio = { version = "^1.38", features = ["io-util", "macros", "net", "rt", "rt-multi-thread"] } 38 | 39 | [features] 40 | async = ["dep:tokio"] 41 | default = [] 42 | 43 | [package.metadata.docs.rs] 44 | features = ["async"] 45 | -------------------------------------------------------------------------------- /ul/README.md: -------------------------------------------------------------------------------- 1 | # DICOM-rs `ul` 2 | 3 | [![CratesIO](https://img.shields.io/crates/v/dicom-ul.svg)](https://crates.io/crates/dicom-ul) 4 | [![Documentation](https://docs.rs/dicom-ul/badge.svg)](https://docs.rs/dicom-ul) 5 | 6 | This is an implementation of the DICOM upper layer protocol. 7 | -------------------------------------------------------------------------------- /ul/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | -------------------------------------------------------------------------------- /ul/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dicom-ul-fuzz" 3 | version = "0.0.0" 4 | description = "Fuzz testing for the dicom-ul crate" 5 | authors = [] 6 | publish = false 7 | edition = "2021" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.dicom-ul] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "pdu_roundtrip" 24 | path = "fuzz_targets/pdu_roundtrip.rs" 25 | test = false 26 | doc = false 27 | 28 | [profile.release] 29 | debug = true 30 | -------------------------------------------------------------------------------- /ul/fuzz/fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | env ASAN_OPTIONS=allocator_may_return_null=1:max_allocation_size_mb=40 cargo +nightly fuzz run pdu_roundtrip 4 | -------------------------------------------------------------------------------- /ul/fuzz/fuzz_targets/pdu_roundtrip.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use std::error::Error; 3 | 4 | use libfuzzer_sys::fuzz_target; 5 | 6 | fuzz_target!(|data: (u32, bool, &[u8])| { 7 | let (maxlen, strict, data) = data; 8 | let _ = fuzz(maxlen, strict, data); 9 | }); 10 | 11 | fn fuzz(maxlen: u32, strict: bool, mut data: &[u8]) -> Result<(), Box> { 12 | // deserialize random bytes 13 | let Some(pdu) = dicom_ul::pdu::read_pdu(&mut data, maxlen, strict)? else { 14 | return Ok(()); 15 | }; 16 | 17 | // serialize pdu back to bytes 18 | let mut bytes = Vec::new(); 19 | dicom_ul::pdu::write_pdu(&mut bytes, &pdu)?; 20 | 21 | // deserialize back to pdu 22 | let pdu2 = dicom_ul::pdu::read_pdu(&mut bytes.as_slice(), maxlen, strict) 23 | .expect("serialized pdu should always deserialize") 24 | .expect("serialized pdu should exist"); 25 | 26 | // assert equivalence 27 | assert_eq!( 28 | pdu, pdu2, 29 | "pdu should be equal after serializing to/from bytes" 30 | ); 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /ul/src/association/mod.rs: -------------------------------------------------------------------------------- 1 | //! DICOM association module 2 | //! 3 | //! This module contains utilities for establishing associations 4 | //! between DICOM nodes via TCP/IP. 5 | //! 6 | //! As an association requester, often as a service class user (SCU), 7 | //! a new association can be started 8 | //! via the [`ClientAssociationOptions`] type. 9 | //! The minimum required properties are the accepted abstract syntaxes 10 | //! and the TCP socket address to the target node. 11 | //! 12 | //! As an association acceptor, 13 | //! usually taking the role of a service class provider (SCP), 14 | //! a newly created [TCP stream][1] can be passed to 15 | //! a previously prepared [`ServerAssociationOptions`]. 16 | //! 17 | //! 18 | //! [1]: std::net::TcpStream 19 | pub mod client; 20 | pub mod server; 21 | 22 | mod uid; 23 | 24 | pub(crate) mod pdata; 25 | 26 | pub use client::{ClientAssociation, ClientAssociationOptions}; 27 | #[cfg(feature = "async")] 28 | pub use pdata::non_blocking::AsyncPDataWriter; 29 | pub use pdata::{PDataReader, PDataWriter}; 30 | pub use server::{ServerAssociation, ServerAssociationOptions}; 31 | -------------------------------------------------------------------------------- /ul/src/association/uid.rs: -------------------------------------------------------------------------------- 1 | //! Private utility module for working with UIDs 2 | 3 | use std::borrow::Cow; 4 | 5 | pub(crate) fn trim_uid(uid: Cow) -> Cow { 6 | if uid.ends_with('\0') { 7 | Cow::Owned( 8 | uid.trim_end_matches(|c: char| c.is_whitespace() || c == '\0') 9 | .to_string(), 10 | ) 11 | } else { 12 | uid 13 | } 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use std::borrow::Cow; 19 | 20 | use super::trim_uid; 21 | 22 | #[test] 23 | fn test_trim_uid() { 24 | let uid = trim_uid(Cow::from("1.2.3.4")); 25 | assert_eq!(uid, "1.2.3.4"); 26 | let uid = trim_uid(Cow::from("1.2.3.4\0")); 27 | assert_eq!(uid, "1.2.3.4"); 28 | let uid = trim_uid(Cow::from("1.2.3.45\0")); 29 | assert_eq!(uid, "1.2.3.45"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ul/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crates contains the types and methods needed to interact 2 | //! with DICOM nodes through the upper layer protocol. 3 | //! 4 | //! This crate can be used as a base 5 | //! for finite-state machines and higher-level helpers, 6 | //! enabling the creation of concrete service class users (SCUs) 7 | //! and service class providers (SCPs). 8 | //! 9 | //! - The [`address`] module 10 | //! provides an abstraction for working with compound addresses 11 | //! referring to application entities in a network. 12 | //! - The [`pdu`] module 13 | //! provides data structures representing _protocol data units_, 14 | //! which are passed around as part of the DICOM network communication support. 15 | //! - The [`association`] module 16 | //! comprises abstractions for establishing and negotiating associations 17 | //! between application entities, 18 | //! via the upper layer protocol by TCP. 19 | //! 20 | //! ## Features 21 | //! * `async`: Enables a fully async implementation of the upper layer protocol. 22 | //! See [`ClientAssociationOptions`] and [`ServerAssociationOptions`] for details 23 | 24 | pub mod address; 25 | pub mod association; 26 | pub mod pdu; 27 | 28 | /// The current implementation class UID generically referring to DICOM-rs. 29 | /// 30 | /// Automatically generated as per the standard, part 5, section B.2. 31 | /// 32 | /// This UID may change in future versions, 33 | /// even between patch versions. 34 | pub const IMPLEMENTATION_CLASS_UID: &str = "2.25.262086406829110419931297894772577063974"; 35 | 36 | /// The current implementation version name generically referring to DICOM-rs. 37 | /// 38 | /// This name may change in future versions, 39 | /// even between patch versions. 40 | pub const IMPLEMENTATION_VERSION_NAME: &str = "DICOM-rs 0.8.1"; 41 | 42 | // re-exports 43 | 44 | pub use address::{AeAddr, FullAeAddr}; 45 | pub use association::client::{ClientAssociation, ClientAssociationOptions}; 46 | pub use association::server::{ServerAssociation, ServerAssociationOptions}; 47 | pub use pdu::read_pdu; 48 | pub use pdu::write_pdu; 49 | pub use pdu::Pdu; 50 | -------------------------------------------------------------------------------- /ul/test.json: -------------------------------------------------------------------------------- 1 | {"_id": "66d9defb7a74b2ded8614922", "label": "deid-export 2024-09-05 11:40:27", "parent": {"id": "6650dd2f995b66671b4a52a5", "type": "subject"}, "parents": {"group": "susannah", "project": "66464b2e6d2adbc8c94a519e", "subject": "6650dd2f995b66671b4a52a5", "session": null, "acquisition": null}, "created": "2024-09-05T16:40:27.338000Z", "modified": "2024-09-05T16:40:27.410000Z", "timestamp": null, "revision": 2, "inputs": [{"_id": "d3ec367e-5d63-4397-bade-3a706bece949", "name": "deid_dicom_zip.yaml", "type": "source code", "file_id": "6650dda1995b66671b4a52a6", "version": 2, "mimetype": "application/octet-stream", "modality": null, "deid_log_id": null, "deid_log_skip_reason": null, "classification": {}, "tags": [], "provider_id": "63e56e1bc78fb19740fd2499", "parent_ref": {"id": "66d9defb7a74b2ded8614922", "type": "analysis"}, "parents": {"group": "susannah", "project": "66464b2e6d2adbc8c94a519e", "subject": null, "session": null, "acquisition": null, "analysis": null}, "restored_from": null, "restored_by": null, "path": "d3/ec/d3ec367e-5d63-4397-bade-3a706bece949", "reference": null, "origin": {"type": "user", "id": "naterichman@flywheel.io"}, "virus_scan": null, "created": "2024-09-05T16:40:27.350000Z", "modified": "2024-09-05T16:40:27.350000Z", "replaced": null, "deleted": null, "size": 1486, "hash": "bb61057bec02e88980613cd5f3f71bb7ccdd86a752bcd85e84945523b8d8e866b6e4528990968485bbe59e19e89921d7", "client_hash": null, "info": {}, "info_exists": false, "zip_member_count": null, "gear_info": {"name": "deid-export", "version": "1.5.3-rc.2", "id": "66630801e6c7518991f26065"}, "copy_of": null, "original_copy_of": null}], "description": "", "info": {}, "files": [], "notes": [], "tags": [], "job": "66d9defb7a74b2ded8614924", "gear_info": {"id": "66630801e6c7518991f26065", "category": "analysis", "name": "deid-export", "version": "1.5.3-rc.2", "capabilities": null}, "compute_provider_id": null, "join-origin": null, "copy_of": null, "original_copy_of": null, "container_type": "analysis"} 2 | -------------------------------------------------------------------------------- /ul/tests/association.rs: -------------------------------------------------------------------------------- 1 | use dicom_dictionary_std::uids::VERIFICATION; 2 | use dicom_ul::ClientAssociationOptions; 3 | use rstest::rstest; 4 | use std::time::Instant; 5 | 6 | const TIMEOUT_TOLERANCE: u64 = 25; 7 | 8 | #[rstest] 9 | #[case(100)] 10 | #[case(500)] 11 | #[case(1000)] 12 | fn test_slow_association(#[case] timeout: u64) { 13 | let scu_init = ClientAssociationOptions::new() 14 | .with_abstract_syntax(VERIFICATION) 15 | .calling_ae_title("RANDOM") 16 | .read_timeout(std::time::Duration::from_secs(1)) 17 | .connection_timeout(std::time::Duration::from_millis(timeout)); 18 | 19 | let now = Instant::now(); 20 | let _res = scu_init.establish_with("RANDOM@167.167.167.167:11111"); 21 | let elapsed = now.elapsed(); 22 | assert!( 23 | elapsed.as_millis() < (timeout + TIMEOUT_TOLERANCE).into(), 24 | "Elapsed time {}ms exceeded the timeout {}ms", 25 | elapsed.as_millis(), 26 | timeout 27 | ); 28 | } 29 | 30 | #[cfg(feature = "async")] 31 | #[rstest] 32 | #[case(100)] 33 | #[case(500)] 34 | #[case(1000)] 35 | #[tokio::test(flavor = "multi_thread")] 36 | async fn test_slow_association_async(#[case] timeout: u64) { 37 | let scu_init = ClientAssociationOptions::new() 38 | .with_abstract_syntax(VERIFICATION) 39 | .calling_ae_title("RANDOM") 40 | .read_timeout(std::time::Duration::from_secs(1)) 41 | .connection_timeout(std::time::Duration::from_millis(timeout)); 42 | let now = Instant::now(); 43 | let res = scu_init 44 | .establish_with_async("RANDOM@167.167.167.167:11111") 45 | .await; 46 | assert!(res.is_err()); 47 | let elapsed = now.elapsed(); 48 | println!("Elapsed time: {:?}", elapsed); 49 | assert!( 50 | elapsed.as_millis() < (timeout + TIMEOUT_TOLERANCE).into(), 51 | "Elapsed time {}ms exceeded the timeout {}ms", 52 | elapsed.as_millis(), 53 | timeout 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /ul/tests/association_echo.rs: -------------------------------------------------------------------------------- 1 | use dicom_ul::{ 2 | association::client::ClientAssociationOptions, 3 | pdu::{Pdu, PresentationContextResult, PresentationContextResultReason}, 4 | }; 5 | 6 | use std::net::SocketAddr; 7 | 8 | use dicom_ul::association::server::ServerAssociationOptions; 9 | 10 | type Result = std::result::Result>; 11 | 12 | static SCU_AE_TITLE: &str = "ECHO-SCU"; 13 | static SCP_AE_TITLE: &str = "ECHO-SCP"; 14 | 15 | static IMPLICIT_VR_LE: &str = "1.2.840.10008.1.2"; 16 | static EXPLICIT_VR_LE: &str = "1.2.840.10008.1.2.1"; 17 | static JPEG_BASELINE: &str = "1.2.840.10008.1.2.4.50"; 18 | static VERIFICATION_SOP_CLASS: &str = "1.2.840.10008.1.1"; 19 | static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; 20 | 21 | fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { 22 | let listener = std::net::TcpListener::bind("localhost:0")?; 23 | let addr = listener.local_addr()?; 24 | let scp = ServerAssociationOptions::new() 25 | .accept_called_ae_title() 26 | .ae_title(SCP_AE_TITLE) 27 | .with_abstract_syntax(VERIFICATION_SOP_CLASS); 28 | 29 | let h = std::thread::spawn(move || -> Result<()> { 30 | let (stream, _addr) = listener.accept()?; 31 | let mut association = scp.establish(stream)?; 32 | 33 | assert_eq!( 34 | association.presentation_contexts(), 35 | &[ 36 | PresentationContextResult { 37 | id: 1, 38 | reason: PresentationContextResultReason::Acceptance, 39 | transfer_syntax: IMPLICIT_VR_LE.to_string(), 40 | }, 41 | PresentationContextResult { 42 | id: 3, 43 | reason: PresentationContextResultReason::AbstractSyntaxNotSupported, 44 | transfer_syntax: IMPLICIT_VR_LE.to_string(), 45 | } 46 | ], 47 | ); 48 | 49 | // handle one release request 50 | let pdu = association.receive()?; 51 | assert_eq!(pdu, Pdu::ReleaseRQ); 52 | association.send(&Pdu::ReleaseRP)?; 53 | 54 | Ok(()) 55 | }); 56 | Ok((h, addr)) 57 | } 58 | 59 | #[cfg(feature = "async")] 60 | async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { 61 | let listener = tokio::net::TcpListener::bind("localhost:0").await?; 62 | let addr = listener.local_addr()?; 63 | let scp = ServerAssociationOptions::new() 64 | .accept_called_ae_title() 65 | .ae_title(SCP_AE_TITLE) 66 | .with_abstract_syntax(VERIFICATION_SOP_CLASS); 67 | 68 | let h = tokio::spawn(async move { 69 | let (stream, _addr) = listener.accept().await?; 70 | let mut association = scp.establish_async(stream).await?; 71 | 72 | assert_eq!( 73 | association.presentation_contexts(), 74 | &[ 75 | PresentationContextResult { 76 | id: 1, 77 | reason: PresentationContextResultReason::Acceptance, 78 | transfer_syntax: IMPLICIT_VR_LE.to_string(), 79 | }, 80 | PresentationContextResult { 81 | id: 3, 82 | reason: PresentationContextResultReason::AbstractSyntaxNotSupported, 83 | transfer_syntax: IMPLICIT_VR_LE.to_string(), 84 | } 85 | ], 86 | ); 87 | 88 | // handle one release request 89 | let pdu = association.receive().await?; 90 | assert_eq!(pdu, Pdu::ReleaseRQ); 91 | association.send(&Pdu::ReleaseRP).await?; 92 | 93 | Ok(()) 94 | }); 95 | Ok((h, addr)) 96 | } 97 | 98 | /// Run an SCP and an SCU concurrently, negotiate an association and release it. 99 | #[test] 100 | fn scu_scp_association_test() { 101 | let (scp_handle, scp_addr) = spawn_scp().unwrap(); 102 | 103 | let association = ClientAssociationOptions::new() 104 | .calling_ae_title(SCU_AE_TITLE) 105 | .called_ae_title(SCP_AE_TITLE) 106 | .with_presentation_context(VERIFICATION_SOP_CLASS, vec![IMPLICIT_VR_LE, EXPLICIT_VR_LE]) 107 | .with_presentation_context( 108 | DIGITAL_MG_STORAGE_SOP_CLASS, 109 | vec![IMPLICIT_VR_LE, EXPLICIT_VR_LE, JPEG_BASELINE], 110 | ) 111 | .establish(scp_addr) 112 | .unwrap(); 113 | 114 | association 115 | .release() 116 | .expect("did not have a peaceful release"); 117 | 118 | scp_handle 119 | .join() 120 | .expect("SCP panicked") 121 | .expect("Error at the SCP"); 122 | } 123 | 124 | #[cfg(feature = "async")] 125 | #[tokio::test(flavor = "multi_thread")] 126 | async fn scu_scp_asociation_test() { 127 | let (scp_handle, scp_addr) = spawn_scp_async().await.unwrap(); 128 | 129 | let association = ClientAssociationOptions::new() 130 | .calling_ae_title(SCU_AE_TITLE) 131 | .called_ae_title(SCP_AE_TITLE) 132 | .with_presentation_context(VERIFICATION_SOP_CLASS, vec![IMPLICIT_VR_LE, EXPLICIT_VR_LE]) 133 | .with_presentation_context( 134 | DIGITAL_MG_STORAGE_SOP_CLASS, 135 | vec![IMPLICIT_VR_LE, EXPLICIT_VR_LE, JPEG_BASELINE], 136 | ) 137 | .establish_async(scp_addr) 138 | .await 139 | .unwrap(); 140 | 141 | association 142 | .release() 143 | .await 144 | .expect("did not have a peaceful release"); 145 | 146 | scp_handle 147 | .await 148 | .expect("SCP panicked") 149 | .expect("Error at the SCP"); 150 | } 151 | -------------------------------------------------------------------------------- /ul/tests/association_store.rs: -------------------------------------------------------------------------------- 1 | use dicom_ul::{ 2 | association::client::ClientAssociationOptions, 3 | pdu::{Pdu, PresentationContextResult, PresentationContextResultReason}, 4 | }; 5 | use std::net::SocketAddr; 6 | 7 | use dicom_ul::association::server::ServerAssociationOptions; 8 | 9 | type Result = std::result::Result>; 10 | 11 | static SCU_AE_TITLE: &str = "STORE-SCU"; 12 | static SCP_AE_TITLE: &str = "STORE-SCP"; 13 | 14 | static IMPLICIT_VR_LE: &str = "1.2.840.10008.1.2"; 15 | static JPEG_BASELINE: &str = "1.2.840.10008.1.2.4.50"; 16 | // raw UID with even padding 17 | // as potentially provided by DICOM objects 18 | static MR_IMAGE_STORAGE_RAW: &str = "1.2.840.10008.5.1.4.1.1.4\0"; 19 | static MR_IMAGE_STORAGE: &str = "1.2.840.10008.5.1.4.1.1.4"; 20 | static DIGITAL_MG_STORAGE_SOP_CLASS_RAW: &str = "1.2.840.10008.5.1.4.1.1.1.2\0"; 21 | static DIGITAL_MG_STORAGE_SOP_CLASS: &str = "1.2.840.10008.5.1.4.1.1.1.2"; 22 | 23 | fn spawn_scp() -> Result<(std::thread::JoinHandle>, SocketAddr)> { 24 | let listener = std::net::TcpListener::bind("localhost:0")?; 25 | let addr = listener.local_addr()?; 26 | let scp = ServerAssociationOptions::new() 27 | .accept_called_ae_title() 28 | .ae_title(SCP_AE_TITLE) 29 | .with_abstract_syntax(MR_IMAGE_STORAGE) 30 | .with_abstract_syntax(DIGITAL_MG_STORAGE_SOP_CLASS); 31 | 32 | let h = std::thread::spawn(move || -> Result<()> { 33 | let (stream, _addr) = listener.accept()?; 34 | let mut association = scp.establish(stream)?; 35 | 36 | assert_eq!( 37 | association.presentation_contexts(), 38 | &[ 39 | PresentationContextResult { 40 | id: 1, 41 | reason: PresentationContextResultReason::Acceptance, 42 | transfer_syntax: IMPLICIT_VR_LE.to_string(), 43 | }, 44 | PresentationContextResult { 45 | id: 3, 46 | reason: PresentationContextResultReason::Acceptance, 47 | transfer_syntax: JPEG_BASELINE.to_string(), 48 | } 49 | ], 50 | ); 51 | 52 | // handle one release request 53 | let pdu = association.receive()?; 54 | assert_eq!(pdu, Pdu::ReleaseRQ); 55 | association.send(&Pdu::ReleaseRP)?; 56 | 57 | Ok(()) 58 | }); 59 | Ok((h, addr)) 60 | } 61 | 62 | #[cfg(feature = "async")] 63 | async fn spawn_scp_async() -> Result<(tokio::task::JoinHandle>, SocketAddr)> { 64 | let listener = tokio::net::TcpListener::bind("localhost:0").await?; 65 | let addr = listener.local_addr()?; 66 | let scp = ServerAssociationOptions::new() 67 | .accept_called_ae_title() 68 | .ae_title(SCP_AE_TITLE) 69 | .with_abstract_syntax(MR_IMAGE_STORAGE) 70 | .with_abstract_syntax(DIGITAL_MG_STORAGE_SOP_CLASS); 71 | 72 | let h = tokio::task::spawn(async move { 73 | let (stream, _addr) = listener.accept().await?; 74 | let mut association = scp.establish_async(stream).await?; 75 | 76 | assert_eq!( 77 | association.presentation_contexts(), 78 | &[ 79 | PresentationContextResult { 80 | id: 1, 81 | reason: PresentationContextResultReason::Acceptance, 82 | transfer_syntax: IMPLICIT_VR_LE.to_string(), 83 | }, 84 | PresentationContextResult { 85 | id: 3, 86 | reason: PresentationContextResultReason::Acceptance, 87 | transfer_syntax: JPEG_BASELINE.to_string(), 88 | } 89 | ], 90 | ); 91 | 92 | // handle one release request 93 | let pdu = association.receive().await?; 94 | assert_eq!(pdu, Pdu::ReleaseRQ); 95 | association.send(&Pdu::ReleaseRP).await?; 96 | 97 | Ok(()) 98 | }); 99 | Ok((h, addr)) 100 | } 101 | 102 | /// Run an SCP and an SCU concurrently, 103 | /// negotiate an association with distinct transfer syntaxes 104 | /// and release it. 105 | #[test] 106 | fn scu_scp_association_test() { 107 | let (scp_handle, scp_addr) = spawn_scp().unwrap(); 108 | 109 | let association = ClientAssociationOptions::new() 110 | .calling_ae_title(SCU_AE_TITLE) 111 | .called_ae_title(SCP_AE_TITLE) 112 | .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) 113 | // MG storage, JPEG baseline 114 | .with_presentation_context(DIGITAL_MG_STORAGE_SOP_CLASS_RAW, vec![JPEG_BASELINE]) 115 | .establish(scp_addr) 116 | .unwrap(); 117 | 118 | for pc in association.presentation_contexts() { 119 | match pc.id { 120 | 1 => { 121 | // guaranteed to be MR image storage 122 | assert_eq!(pc.transfer_syntax, IMPLICIT_VR_LE); 123 | } 124 | 3 => { 125 | // guaranteed to be MG image storage 126 | assert_eq!(pc.transfer_syntax, JPEG_BASELINE); 127 | } 128 | id => panic!("unexpected presentation context ID {}", id), 129 | } 130 | } 131 | 132 | association 133 | .release() 134 | .expect("did not have a peaceful release"); 135 | 136 | scp_handle 137 | .join() 138 | .expect("SCP panicked") 139 | .expect("Error at the SCP"); 140 | } 141 | 142 | #[cfg(feature = "async")] 143 | #[tokio::test(flavor = "multi_thread")] 144 | async fn scu_scp_association_test_async() { 145 | let (scp_handle, scp_addr) = spawn_scp_async().await.unwrap(); 146 | 147 | let association = ClientAssociationOptions::new() 148 | .calling_ae_title(SCU_AE_TITLE) 149 | .called_ae_title(SCP_AE_TITLE) 150 | .with_presentation_context(MR_IMAGE_STORAGE_RAW, vec![IMPLICIT_VR_LE]) 151 | // MG storage, JPEG baseline 152 | .with_presentation_context(DIGITAL_MG_STORAGE_SOP_CLASS_RAW, vec![JPEG_BASELINE]) 153 | .establish_async(scp_addr) 154 | .await 155 | .unwrap(); 156 | 157 | for pc in association.presentation_contexts() { 158 | match pc.id { 159 | 1 => { 160 | // guaranteed to be MR image storage 161 | assert_eq!(pc.transfer_syntax, IMPLICIT_VR_LE); 162 | } 163 | 3 => { 164 | // guaranteed to be MG image storage 165 | assert_eq!(pc.transfer_syntax, JPEG_BASELINE); 166 | } 167 | id => panic!("unexpected presentation context ID {}", id), 168 | } 169 | } 170 | 171 | association 172 | .release() 173 | .await 174 | .expect("did not have a peaceful release"); 175 | 176 | scp_handle 177 | .await 178 | .expect("SCP panicked") 179 | .expect("Error at the SCP"); 180 | } 181 | --------------------------------------------------------------------------------