PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
32
33#include <algorithm>
34#include <atomic>
35#include <chrono>
36#include <cstdlib>
37#include <filesystem>
38#include <fstream>
39#include <iomanip>
40#include <iostream>
41#include <regex>
42#include <sstream>
43#include <string>
44#include <vector>
45
46namespace {
47
48// =============================================================================
49// Constants
50// =============================================================================
51
52constexpr const char* version_string = "1.0.0";
53constexpr const char* default_calling_ae = "GETSCU";
54constexpr const char* default_called_ae = "ANY-SCP";
55constexpr auto default_timeout = std::chrono::seconds{60};
56constexpr size_t max_ae_title_length = 16;
57constexpr int progress_bar_width = 40;
58
59// Storage SOP Classes for role negotiation
60static const std::vector<std::string_view> storage_sop_classes = {
61 "1.2.840.10008.5.1.4.1.1.2", // CT Image Storage
62 "1.2.840.10008.5.1.4.1.1.4", // MR Image Storage
63 "1.2.840.10008.5.1.4.1.1.7", // Secondary Capture Image Storage
64 "1.2.840.10008.5.1.4.1.1.1", // CR Image Storage
65 "1.2.840.10008.5.1.4.1.1.1.1", // Digital X-Ray Image Storage
66 "1.2.840.10008.5.1.4.1.1.12.1", // X-Ray Angiographic Image Storage
67 "1.2.840.10008.5.1.4.1.1.6.1", // US Image Storage
68 "1.2.840.10008.5.1.4.1.1.88.11", // Basic Text SR
69 "1.2.840.10008.5.1.4.1.1.88.22", // Enhanced SR
70 "1.2.840.10008.5.1.4.1.1.2.1", // Enhanced CT Image Storage
71 "1.2.840.10008.5.1.4.1.1.4.1", // Enhanced MR Image Storage
72 "1.2.840.10008.5.1.4.1.1.128", // PET Image Storage
73};
74
75// =============================================================================
76// Query Model and Level
77// =============================================================================
78
79enum class query_model {
82};
83
84enum class query_level {
85 patient,
86 study,
87 series,
88 image
89};
90
91// =============================================================================
92// Query Key
93// =============================================================================
94
95struct query_key {
97 std::string value;
98};
99
100// =============================================================================
101// Command Line Options
102// =============================================================================
103
104struct options {
105 // Network options
106 std::string peer_host;
107 uint16_t peer_port{0};
108 std::string calling_ae_title{default_calling_ae};
109 std::string called_ae_title{default_called_ae};
110
111 // Timeout options
112 std::chrono::seconds connection_timeout{default_timeout};
113 std::chrono::seconds acse_timeout{default_timeout};
114 std::chrono::seconds dimse_timeout{0};
115
116 // Query model and level
117 query_model model{query_model::study_root};
118 query_level level{query_level::study};
119
120 // Query keys
121 std::vector<query_key> keys;
122 std::string query_file;
123
124 // Output options
125 std::filesystem::path output_dir{"."};
126 std::string output_format; // Filename format
127
128 // Transfer syntax preferences
129 bool prefer_lossless{false};
130 bool prefer_explicit{false};
131 bool accept_all{false};
132
133 // Progress options
134 bool show_progress{true};
135
136 // Verbosity
137 bool verbose{false};
138 bool debug{false};
139 bool quiet{false};
140
141 // Help/version flags
142 bool show_help{false};
143 bool show_version{false};
144};
145
146// =============================================================================
147// Progress Tracking
148// =============================================================================
149
150struct get_progress {
151 std::atomic<uint16_t> remaining{0};
152 std::atomic<uint16_t> completed{0};
153 std::atomic<uint16_t> failed{0};
154 std::atomic<uint16_t> warning{0};
155 std::atomic<size_t> bytes_received{0};
156 std::chrono::steady_clock::time_point start_time;
157
158 void reset() {
159 remaining = 0;
160 completed = 0;
161 failed = 0;
162 warning = 0;
163 bytes_received = 0;
164 start_time = std::chrono::steady_clock::now();
165 }
166
167 [[nodiscard]] uint16_t total() const {
168 return remaining + completed + failed + warning;
169 }
170};
171
172// =============================================================================
173// Utility Functions
174// =============================================================================
175
176std::string_view query_model_to_string(query_model model) {
177 switch (model) {
178 case query_model::patient_root: return "Patient Root";
179 case query_model::study_root: return "Study Root";
180 default: return "Unknown";
181 }
182}
183
184std::string_view query_level_to_string(query_level level) {
185 switch (level) {
186 case query_level::patient: return "PATIENT";
187 case query_level::study: return "STUDY";
188 case query_level::series: return "SERIES";
189 case query_level::image: return "IMAGE";
190 default: return "UNKNOWN";
191 }
192}
193
194std::string_view get_get_sop_class_uid(query_model model) {
195 switch (model) {
196 case query_model::patient_root:
198 case query_model::study_root:
200 default:
202 }
203}
204
205std::string format_size(size_t bytes) {
206 constexpr size_t KB = 1024;
207 constexpr size_t MB = KB * 1024;
208 constexpr size_t GB = MB * 1024;
209
210 std::ostringstream oss;
211 oss << std::fixed << std::setprecision(2);
212
213 if (bytes >= GB) {
214 oss << static_cast<double>(bytes) / GB << " GB";
215 } else if (bytes >= MB) {
216 oss << static_cast<double>(bytes) / MB << " MB";
217 } else if (bytes >= KB) {
218 oss << static_cast<double>(bytes) / KB << " KB";
219 } else {
220 oss << bytes << " B";
221 }
222
223 return oss.str();
224}
225
226// =============================================================================
227// Output Functions
228// =============================================================================
229
230void print_banner() {
231 std::cout << R"(
232 ____ _____ _____ ____ ____ _ _
233 / ___| ____|_ _| / ___| / ___| | | |
234 | | _| _| | | \___ \| | | | | |
235 | |_| | |___ | | ___) | |___| |_| |
236 \____|_____| |_| |____/ \____|\___/
237
238 DICOM C-GET Client v)" << version_string << R"(
239)" << "\n";
240}
241
242void print_usage(const char* program_name) {
243 std::cout << "Usage: " << program_name << R"( [options] <peer> <port>
244
245Arguments:
246 peer Remote host address (IP or hostname)
247 port Remote port number (typically 104 or 11112)
248
249Options:
250 -h, --help Show this help message and exit
251 -v, --verbose Verbose output mode
252 -d, --debug Debug output mode
253 -q, --quiet Quiet mode (minimal output)
254 --version Show version information
255
256Network Options:
257 -aet, --aetitle <aetitle> Calling AE Title (default: GETSCU)
258 -aec, --call <aetitle> Called AE Title (default: ANY-SCP)
259 -to, --timeout <seconds> Connection timeout (default: 60)
260 -ta, --acse-timeout <seconds> ACSE timeout (default: 60)
261 -td, --dimse-timeout <seconds> DIMSE timeout (default: 0=infinite)
262
263Query Model:
264 -P, --patient-root Patient Root Query Model
265 -S, --study-root Study Root Query Model (default)
266
267Query Level:
268 -L, --level <level> Retrieve level (PATIENT|STUDY|SERIES|IMAGE)
269
270Query Keys:
271 -k, --key <tag=value> Query key for retrieval
272 -f, --query-file <file> Read query keys from file
273
274Output Options:
275 -od, --output-dir <dir> Output directory (default: current)
276 --output-format <format> Filename format
277
278Storage Options:
279 -xs, --prefer-lossless Prefer lossless transfer syntax
280 -xe, --prefer-explicit Prefer Explicit VR LE
281 +xa, --accept-all Accept all transfer syntaxes
282
283Progress Options:
284 -p, --progress Show progress information (default)
285 --no-progress Disable progress display
286
287C-GET vs C-MOVE:
288 C-GET retrieves objects directly to the calling SCU, eliminating
289 the need for a separate storage SCP. This makes it firewall-friendly
290 but requires SCP support for C-GET (less common than C-MOVE).
291
292Examples:
293 # Get single instance
294 )" << program_name << R"( -L IMAGE \
295 -k "0008,0018=1.2.840..." \
296 -od ./retrieved/ \
297 localhost 11112
298
299 # Get entire study
300 )" << program_name << R"( -L STUDY \
301 -k "0020,000D=1.2.840..." \
302 --progress \
303 -od ./study_data/ \
304 pacs.example.com 104
305
306 # Get with lossless preference
307 )" << program_name << R"( --prefer-lossless \
308 -L SERIES \
309 -k "0020,000E=1.2.840..." \
310 localhost 11112
311
312Exit Codes:
313 0 Success - All objects retrieved
314 1 Partial success - Some sub-operations failed
315 2 Error - Retrieve failed or invalid arguments
316)";
317}
318
319void print_version() {
320 std::cout << "get_scu version " << version_string << "\n";
321 std::cout << "PACS System DICOM Utilities\n";
322 std::cout << "Copyright (c) 2024\n";
323}
324
325void display_progress(const get_progress& progress, bool verbose) {
326 auto total = progress.total();
327 if (total == 0) return;
328
329 uint16_t done = progress.completed + progress.failed + progress.warning;
330 float pct = static_cast<float>(done) / total;
331
332 auto elapsed = std::chrono::steady_clock::now() - progress.start_time;
333 auto elapsed_sec = std::chrono::duration<double>(elapsed).count();
334 double speed = elapsed_sec > 0
335 ? static_cast<double>(progress.bytes_received) / elapsed_sec / 1024.0
336 : 0;
337
338 std::cout << "\r[";
339 int filled = static_cast<int>(pct * progress_bar_width);
340 for (int i = 0; i < progress_bar_width; ++i) {
341 if (i < filled) std::cout << "=";
342 else if (i == filled) std::cout << ">";
343 else std::cout << " ";
344 }
345 std::cout << "] ";
346
347 std::cout << std::fixed << std::setprecision(1) << (pct * 100) << "% ";
348 std::cout << "(" << done << "/" << total << ") ";
349
350 if (verbose) {
351 std::cout << std::setprecision(1) << speed << " KB/s ";
352 if (progress.failed > 0) {
353 std::cout << "[" << progress.failed.load() << " failed] ";
354 }
355 }
356
357 std::cout << std::flush;
358}
359
360// =============================================================================
361// Argument Parsing
362// =============================================================================
363
364bool parse_timeout(const std::string& value, std::chrono::seconds& result,
365 const std::string& option_name) {
366 try {
367 int seconds = std::stoi(value);
368 if (seconds < 0) {
369 std::cerr << "Error: " << option_name << " must be non-negative\n";
370 return false;
371 }
372 result = std::chrono::seconds{seconds};
373 return true;
374 } catch (const std::exception&) {
375 std::cerr << "Error: Invalid value for " << option_name << ": '"
376 << value << "'\n";
377 return false;
378 }
379}
380
381bool validate_ae_title(const std::string& ae_title,
382 const std::string& option_name) {
383 if (ae_title.empty()) {
384 std::cerr << "Error: " << option_name << " cannot be empty\n";
385 return false;
386 }
387 if (ae_title.length() > max_ae_title_length) {
388 std::cerr << "Error: " << option_name << " exceeds "
389 << max_ae_title_length << " characters\n";
390 return false;
391 }
392 return true;
393}
394
395std::optional<query_level> parse_level(const std::string& level_str) {
396 std::string upper = level_str;
397 std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
398
399 if (upper == "PATIENT") return query_level::patient;
400 if (upper == "STUDY") return query_level::study;
401 if (upper == "SERIES") return query_level::series;
402 if (upper == "IMAGE" || upper == "INSTANCE") return query_level::image;
403 return std::nullopt;
404}
405
406bool parse_query_key(const std::string& key_str, query_key& key) {
407 std::regex key_regex(R"(\‍(?([0-9A-Fa-f]{4}),([0-9A-Fa-f]{4})\)?=?(.*))");
408 std::smatch match;
409
410 if (!std::regex_match(key_str, match, key_regex)) {
411 std::cerr << "Error: Invalid query key format: '" << key_str << "'\n";
412 return false;
413 }
414
415 uint16_t group = static_cast<uint16_t>(std::stoul(match[1].str(), nullptr, 16));
416 uint16_t element = static_cast<uint16_t>(std::stoul(match[2].str(), nullptr, 16));
417
418 key.tag = kcenon::pacs::core::dicom_tag{group, element};
419 key.value = match[3].str();
420
421 return true;
422}
423
424bool load_query_file(const std::string& filename, std::vector<query_key>& keys) {
425 std::ifstream file(filename);
426 if (!file.is_open()) {
427 std::cerr << "Error: Cannot open query file: " << filename << "\n";
428 return false;
429 }
430
431 std::string line;
432 while (std::getline(file, line)) {
433 auto pos = line.find_first_not_of(" \t");
434 if (pos == std::string::npos || line[pos] == '#') {
435 continue;
436 }
437
438 query_key key;
439 if (!parse_query_key(line, key)) {
440 return false;
441 }
442 keys.push_back(key);
443 }
444
445 return true;
446}
447
448bool parse_arguments(int argc, char* argv[], options& opts) {
449 std::vector<std::string> positional_args;
450
451 for (int i = 1; i < argc; ++i) {
452 std::string arg = argv[i];
453
454 if (arg == "-h" || arg == "--help") {
455 opts.show_help = true;
456 return true;
457 }
458 if (arg == "--version") {
459 opts.show_version = true;
460 return true;
461 }
462
463 if (arg == "-v" || arg == "--verbose") {
464 opts.verbose = true;
465 continue;
466 }
467 if (arg == "-d" || arg == "--debug") {
468 opts.debug = true;
469 opts.verbose = true;
470 continue;
471 }
472 if (arg == "-q" || arg == "--quiet") {
473 opts.quiet = true;
474 continue;
475 }
476
477 if ((arg == "-aet" || arg == "--aetitle") && i + 1 < argc) {
478 opts.calling_ae_title = argv[++i];
479 if (!validate_ae_title(opts.calling_ae_title, "Calling AE Title")) {
480 return false;
481 }
482 continue;
483 }
484 if ((arg == "-aec" || arg == "--call") && i + 1 < argc) {
485 opts.called_ae_title = argv[++i];
486 if (!validate_ae_title(opts.called_ae_title, "Called AE Title")) {
487 return false;
488 }
489 continue;
490 }
491
492 if ((arg == "-to" || arg == "--timeout") && i + 1 < argc) {
493 if (!parse_timeout(argv[++i], opts.connection_timeout, "timeout")) {
494 return false;
495 }
496 continue;
497 }
498 if ((arg == "-ta" || arg == "--acse-timeout") && i + 1 < argc) {
499 if (!parse_timeout(argv[++i], opts.acse_timeout, "ACSE timeout")) {
500 return false;
501 }
502 continue;
503 }
504 if ((arg == "-td" || arg == "--dimse-timeout") && i + 1 < argc) {
505 if (!parse_timeout(argv[++i], opts.dimse_timeout, "DIMSE timeout")) {
506 return false;
507 }
508 continue;
509 }
510
511 if (arg == "-P" || arg == "--patient-root") {
512 opts.model = query_model::patient_root;
513 continue;
514 }
515 if (arg == "-S" || arg == "--study-root") {
516 opts.model = query_model::study_root;
517 continue;
518 }
519
520 if ((arg == "-L" || arg == "--level") && i + 1 < argc) {
521 auto level = parse_level(argv[++i]);
522 if (!level) {
523 std::cerr << "Error: Invalid query level: '" << argv[i] << "'\n";
524 return false;
525 }
526 opts.level = *level;
527 continue;
528 }
529
530 if ((arg == "-k" || arg == "--key") && i + 1 < argc) {
531 query_key key;
532 if (!parse_query_key(argv[++i], key)) {
533 return false;
534 }
535 opts.keys.push_back(key);
536 continue;
537 }
538 if ((arg == "-f" || arg == "--query-file") && i + 1 < argc) {
539 opts.query_file = argv[++i];
540 continue;
541 }
542
543 if ((arg == "-od" || arg == "--output-dir") && i + 1 < argc) {
544 opts.output_dir = argv[++i];
545 continue;
546 }
547 if (arg == "--output-format" && i + 1 < argc) {
548 opts.output_format = argv[++i];
549 continue;
550 }
551
552 if (arg == "-xs" || arg == "--prefer-lossless") {
553 opts.prefer_lossless = true;
554 continue;
555 }
556 if (arg == "-xe" || arg == "--prefer-explicit") {
557 opts.prefer_explicit = true;
558 continue;
559 }
560 if (arg == "+xa" || arg == "--accept-all") {
561 opts.accept_all = true;
562 continue;
563 }
564
565 if (arg == "-p" || arg == "--progress") {
566 opts.show_progress = true;
567 continue;
568 }
569 if (arg == "--no-progress") {
570 opts.show_progress = false;
571 continue;
572 }
573
574 if (arg.starts_with("-")) {
575 std::cerr << "Error: Unknown option '" << arg << "'\n";
576 return false;
577 }
578
579 positional_args.push_back(arg);
580 }
581
582 if (positional_args.size() != 2) {
583 std::cerr << "Error: Expected <peer> <port> arguments\n";
584 return false;
585 }
586
587 opts.peer_host = positional_args[0];
588
589 try {
590 int port_int = std::stoi(positional_args[1]);
591 if (port_int < 1 || port_int > 65535) {
592 std::cerr << "Error: Port must be between 1 and 65535\n";
593 return false;
594 }
595 opts.peer_port = static_cast<uint16_t>(port_int);
596 } catch (...) {
597 std::cerr << "Error: Invalid port number '" << positional_args[1] << "'\n";
598 return false;
599 }
600
601 if (!opts.query_file.empty()) {
602 if (!load_query_file(opts.query_file, opts.keys)) {
603 return false;
604 }
605 }
606
607 if (opts.keys.empty()) {
608 std::cerr << "Error: At least one query key (-k) is required\n";
609 return false;
610 }
611
612 return true;
613}
614
615// =============================================================================
616// Query Dataset Building
617// =============================================================================
618
619kcenon::pacs::core::dicom_dataset build_query_dataset(const options& opts) {
620 using namespace kcenon::pacs::core;
621 using namespace kcenon::pacs::encoding;
622
623 dicom_dataset ds;
624
625 std::string level_str{query_level_to_string(opts.level)};
626 ds.set_string(tags::query_retrieve_level, vr_type::CS, level_str);
627
628 for (const auto& key : opts.keys) {
629 ds.set_string(key.tag, vr_type::UN, key.value);
630 }
631
632 return ds;
633}
634
635// =============================================================================
636// File Path Generation
637// =============================================================================
638
639std::filesystem::path generate_file_path(
640 const std::filesystem::path& output_dir,
641 const kcenon::pacs::core::dicom_dataset& dataset) {
642
643 using namespace kcenon::pacs::core;
644
645 auto sop_uid = dataset.get_string(tags::sop_instance_uid, "UNKNOWN");
646
647 // Replace any characters that might be invalid in filenames
648 std::string filename = sop_uid + ".dcm";
649 std::replace(filename.begin(), filename.end(), '/', '_');
650 std::replace(filename.begin(), filename.end(), '\\', '_');
651
652 return output_dir / filename;
653}
654
655bool save_dicom_file(const std::filesystem::path& path,
656 const kcenon::pacs::core::dicom_dataset& dataset) {
657 std::filesystem::create_directories(path.parent_path());
658
660 dataset,
662
663 auto result = file.save(path);
664 return result.is_ok();
665}
666
667// =============================================================================
668// Get Implementation
669// =============================================================================
670
672 uint16_t message_id,
673 std::string_view sop_class_uid) {
674
675 using namespace kcenon::pacs::network::dimse;
676
677 dimse_message msg{command_field::c_get_rq, message_id};
678 msg.set_affected_sop_class_uid(sop_class_uid);
679 msg.set_priority(priority_medium);
680
681 return msg;
682}
683
684int perform_get(const options& opts) {
685 using namespace kcenon::pacs::network;
686 using namespace kcenon::pacs::network::dimse;
687 using namespace kcenon::pacs::services;
688
689 auto sop_class_uid = get_get_sop_class_uid(opts.model);
690
691 if (!opts.quiet) {
692 std::cout << "Requesting Association\n";
693 if (opts.verbose) {
694 std::cout << " Peer: " << opts.peer_host << ":"
695 << opts.peer_port << "\n";
696 std::cout << " Calling AE: " << opts.calling_ae_title << "\n";
697 std::cout << " Called AE: " << opts.called_ae_title << "\n";
698 std::cout << " Query Model: " << query_model_to_string(opts.model)
699 << "\n";
700 std::cout << " Query Level: " << query_level_to_string(opts.level)
701 << "\n";
702 std::cout << " Output: " << opts.output_dir << "\n\n";
703 }
704 }
705
706 std::filesystem::create_directories(opts.output_dir);
707
708 // Configure association
709 association_config config;
710 config.calling_ae_title = opts.calling_ae_title;
711 config.called_ae_title = opts.called_ae_title;
712 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
713 config.implementation_version_name = "GET_SCU_100";
714
715 // Propose C-GET SOP Class
716 config.proposed_contexts.push_back({
717 1,
718 std::string(sop_class_uid),
719 {
720 "1.2.840.10008.1.2.1",
721 "1.2.840.10008.1.2"
722 }
723 });
724
725 // Propose storage SOP classes for receiving
726 uint8_t context_id = 3;
727 for (auto storage_sop : storage_sop_classes) {
728 config.proposed_contexts.push_back({
729 context_id,
730 std::string(storage_sop),
731 {
732 "1.2.840.10008.1.2.1",
733 "1.2.840.10008.1.2"
734 }
735 });
736 context_id += 2;
737 }
738
739 // Establish association
740 auto start_time = std::chrono::steady_clock::now();
741 auto timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
742 opts.connection_timeout);
743 auto connect_result = association::connect(opts.peer_host, opts.peer_port,
744 config, timeout);
745
746 if (connect_result.is_err()) {
747 std::cerr << "Association Failed: " << connect_result.error().message
748 << "\n";
749 return 2;
750 }
751
752 auto& assoc = connect_result.value();
753
754 if (!opts.quiet) {
755 std::cout << "Association Accepted\n";
756 }
757
758 if (!assoc.has_accepted_context(sop_class_uid)) {
759 std::cerr << "Error: C-GET SOP Class not accepted by remote SCP\n";
760 std::cerr << "Note: C-GET is less commonly supported than C-MOVE\n";
761 assoc.abort();
762 return 2;
763 }
764
765 auto context_id_opt = assoc.accepted_context_id(sop_class_uid);
766 if (!context_id_opt) {
767 std::cerr << "Error: Could not get presentation context ID\n";
768 assoc.abort();
769 return 2;
770 }
771 uint8_t get_context_id = *context_id_opt;
772
773 auto query_ds = build_query_dataset(opts);
774
775 auto get_rq = make_c_get_rq(1, sop_class_uid);
776 get_rq.set_dataset(std::move(query_ds));
777
778 if (!opts.quiet) {
779 std::cout << "Initiating C-GET...\n";
780 }
781
782 auto send_result = assoc.send_dimse(get_context_id, get_rq);
783 if (send_result.is_err()) {
784 std::cerr << "Send Failed: " << send_result.error().message << "\n";
785 assoc.abort();
786 return 2;
787 }
788
789 // Progress tracking
790 get_progress progress;
791 progress.reset();
792
793 std::vector<std::filesystem::path> received_files;
794
795 bool retrieve_complete = false;
796 uint16_t final_completed = 0;
797 uint16_t final_failed = 0;
798 uint16_t final_warning = 0;
799
800 auto dimse_timeout = opts.dimse_timeout.count() > 0
801 ? std::chrono::duration_cast<std::chrono::milliseconds>(opts.dimse_timeout)
802 : std::chrono::milliseconds{60000};
803
804 while (!retrieve_complete) {
805 auto recv_result = assoc.receive_dimse(dimse_timeout);
806 if (recv_result.is_err()) {
807 std::cerr << "\nReceive Failed: " << recv_result.error().message
808 << "\n";
809 assoc.abort();
810 return 2;
811 }
812
813 auto& [recv_context_id, msg] = recv_result.value();
814 auto cmd = msg.command();
815
816 if (cmd == command_field::c_get_rsp) {
817 auto status = msg.status();
818
819 if (auto remaining = msg.remaining_subops()) {
820 progress.remaining = *remaining;
821 }
822 if (auto completed = msg.completed_subops()) {
823 progress.completed = *completed;
824 final_completed = *completed;
825 }
826 if (auto failed = msg.failed_subops()) {
827 progress.failed = *failed;
828 final_failed = *failed;
829 }
830 if (auto warning = msg.warning_subops()) {
831 progress.warning = *warning;
832 final_warning = *warning;
833 }
834
835 if (opts.show_progress && !opts.quiet) {
836 display_progress(progress, opts.verbose);
837 }
838
839 if (status == status_success ||
840 status == status_cancel ||
841 (status & 0xF000) == 0xA000 ||
842 (status & 0xF000) == 0xC000) {
843
844 retrieve_complete = true;
845
846 if (status != status_success && status != status_cancel &&
847 !opts.quiet) {
848 std::cerr << "\nC-GET failed with status: 0x" << std::hex
849 << status << std::dec << "\n";
850 }
851 }
852
853 } else if (cmd == command_field::c_store_rq) {
854 // Incoming C-STORE sub-operation
855 if (msg.has_dataset()) {
856 auto dataset_result = msg.dataset();
857 if (dataset_result.is_err()) {
858 if (opts.verbose) {
859 std::cerr << "\nWarning: Failed to get dataset\n";
860 }
861 continue;
862 }
863 const auto& dataset = dataset_result.value().get();
864
865 auto file_path = generate_file_path(opts.output_dir, dataset);
866 bool saved = save_dicom_file(file_path, dataset);
867
868 if (saved) {
869 received_files.push_back(file_path);
870 }
871
872 // Approximate bytes received
873 progress.bytes_received += 1024;
874
875 auto sop_class = msg.affected_sop_class_uid();
876 auto sop_instance = msg.affected_sop_instance_uid();
877
878 auto store_rsp = make_c_store_rsp(
879 msg.message_id(),
880 sop_class,
881 sop_instance,
882 saved ? status_success : 0xA700
883 );
884
885 auto send_rsp_result = assoc.send_dimse(recv_context_id, store_rsp);
886 if (send_rsp_result.is_err() && opts.verbose) {
887 std::cerr << "\nWarning: Failed to send C-STORE response\n";
888 }
889
890 if (!saved && opts.verbose) {
891 std::cerr << "\nWarning: Failed to save " << file_path << "\n";
892 }
893 }
894 }
895 }
896
897 if (opts.show_progress && !opts.quiet) {
898 std::cout << "\n";
899 }
900
901 if (!opts.quiet && opts.verbose) {
902 std::cout << "Releasing Association\n";
903 }
904
905 auto release_result = assoc.release(timeout);
906 if (release_result.is_err() && opts.verbose) {
907 std::cerr << "Warning: Release failed: "
908 << release_result.error().message << "\n";
909 }
910
911 auto end_time = std::chrono::steady_clock::now();
912 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
913 end_time - start_time);
914
915 // Print summary
916 if (!opts.quiet) {
917 std::cout << "\n========================================\n";
918 std::cout << " Get Summary\n";
919 std::cout << "========================================\n";
920 std::cout << " Mode: C-GET\n";
921 std::cout << " Level: " << query_level_to_string(opts.level)
922 << "\n";
923 std::cout << " Output: " << opts.output_dir << "\n";
924 std::cout << " ----------------------------------------\n";
925 std::cout << " Received: " << received_files.size() << " files\n";
926 std::cout << " Data Size: " << format_size(progress.bytes_received)
927 << "\n";
928 std::cout << " Completed: " << final_completed << "\n";
929 if (final_warning > 0) {
930 std::cout << " Warnings: " << final_warning << "\n";
931 }
932 if (final_failed > 0) {
933 std::cout << " Failed: " << final_failed << "\n";
934 }
935 std::cout << " Total Time: " << total_duration.count() << " ms\n";
936
937 if (total_duration.count() > 0) {
938 double speed = static_cast<double>(progress.bytes_received) /
939 (total_duration.count() / 1000.0) / (1024.0 * 1024.0);
940 std::cout << std::fixed << std::setprecision(2);
941 std::cout << " Average Speed: " << speed << " MB/s\n";
942 }
943
944 std::cout << "========================================\n";
945 }
946
947 if (final_failed > 0 && final_completed == 0) {
948 return 2;
949 } else if (final_failed > 0) {
950 return 1;
951 }
952 return 0;
953}
954
955} // namespace
956
957// =============================================================================
958// Main Entry Point
959// =============================================================================
960
961int main(int argc, char* argv[]) {
962 options opts;
963
964 if (!parse_arguments(argc, argv, opts)) {
965 if (!opts.show_help && !opts.show_version) {
966 std::cerr << "\nUse --help for usage information.\n";
967 return 2;
968 }
969 }
970
971 if (opts.show_version) {
972 print_version();
973 return 0;
974 }
975
976 if (opts.show_help) {
977 print_banner();
978 print_usage(argv[0]);
979 return 0;
980 }
981
982 if (!opts.quiet) {
983 print_banner();
984 }
985
986 return perform_get(opts);
987}
DICOM Association management per PS3.8.
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
static auto create(dicom_dataset dataset, const encoding::transfer_syntax &ts) -> dicom_file
Create a new DICOM file from a dataset.
static const transfer_syntax explicit_vr_little_endian
Explicit VR Little Endian (1.2.840.10008.1.2.1)
DICOM Dataset - ordered collection of Data Elements.
DICOM Part 10 file handling for reading/writing DICOM files.
DICOM Tag representation (Group, Element pairs)
Compile-time constants for commonly used DICOM tags.
DIMSE message encoding and decoding.
int main()
Definition main.cpp:84
@ completed
Job completed successfully.
constexpr dicom_tag message_id
Message ID.
constexpr dicom_tag status
Status.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr int connection_timeout
Definition result.h:95
auto query_level_to_string(query_level level) -> std::string
Convert query level to string.
Definition events.h:176
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
@ done
Job completed successfully.
constexpr std::string_view study_root_get_sop_class_uid
Study Root Query/Retrieve Information Model - GET.
constexpr std::string_view patient_root_get_sop_class_uid
Patient Root Query/Retrieve Information Model - GET.
@ study_root
Study Root Query/Retrieve Information Model.
@ patient_root
Patient Root Query/Retrieve Information Model.
constexpr int timeout
Lock timeout exceeded.
DICOM Retrieve SCP service (C-MOVE/C-GET handler)
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts