PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
28
29#include <algorithm>
30#include <cctype>
31#include <filesystem>
32#include <iomanip>
33#include <iostream>
34#include <map>
35#include <set>
36#include <sstream>
37#include <string>
38#include <vector>
39
40namespace {
41
42// ============================================================================
43// DICOMDIR Tags (Group 0x0004)
44// ============================================================================
45
46namespace dir_tags {
48inline constexpr kcenon::pacs::core::dicom_tag file_set_id{0x0004, 0x1130};
50inline constexpr kcenon::pacs::core::dicom_tag file_set_descriptor_file_id{0x0004, 0x1141};
52inline constexpr kcenon::pacs::core::dicom_tag specific_character_set_of_file_set{0x0004, 0x1142};
54inline constexpr kcenon::pacs::core::dicom_tag offset_of_first_directory_record{0x0004, 0x1200};
56inline constexpr kcenon::pacs::core::dicom_tag offset_of_last_directory_record{0x0004, 0x1202};
58inline constexpr kcenon::pacs::core::dicom_tag file_set_consistency_flag{0x0004, 0x1212};
60inline constexpr kcenon::pacs::core::dicom_tag directory_record_sequence{0x0004, 0x1220};
62inline constexpr kcenon::pacs::core::dicom_tag offset_of_next_directory_record{0x0004, 0x1400};
64inline constexpr kcenon::pacs::core::dicom_tag record_in_use_flag{0x0004, 0x1410};
66inline constexpr kcenon::pacs::core::dicom_tag offset_of_lower_level_directory_entity{0x0004, 0x1420};
68inline constexpr kcenon::pacs::core::dicom_tag directory_record_type{0x0004, 0x1430};
70inline constexpr kcenon::pacs::core::dicom_tag private_record_uid{0x0004, 0x1432};
72inline constexpr kcenon::pacs::core::dicom_tag referenced_file_id{0x0004, 0x1500};
74inline constexpr kcenon::pacs::core::dicom_tag mrdr_directory_record_offset{0x0004, 0x1504};
76inline constexpr kcenon::pacs::core::dicom_tag referenced_sop_class_uid_in_file{0x0004, 0x1510};
78inline constexpr kcenon::pacs::core::dicom_tag referenced_sop_instance_uid_in_file{0x0004, 0x1511};
80inline constexpr kcenon::pacs::core::dicom_tag referenced_transfer_syntax_uid_in_file{0x0004, 0x1512};
81} // namespace dir_tags
82
83// ============================================================================
84// Constants
85// ============================================================================
86
88constexpr const char* kMediaStorageDirectorySopClassUid = "1.2.840.10008.1.3.10";
89
91constexpr const char* kImplementationClassUid = "1.2.826.0.1.3680043.8.1055.1";
92
94constexpr const char* kImplementationVersionName = "PACS_SYS_001";
95
96// ============================================================================
97// Data Structures
98// ============================================================================
99
103enum class command_type { none, create, list, verify, update };
104
108struct directory_record {
109 std::string type; // PATIENT, STUDY, SERIES, IMAGE
110 std::map<std::string, std::string> attrs; // Key attributes
111 std::filesystem::path file_path; // Referenced file path
112 std::string sop_class_uid; // Referenced SOP Class UID
113 std::string sop_instance_uid; // Referenced SOP Instance UID
114 std::string transfer_syntax_uid; // Referenced Transfer Syntax UID
115 std::vector<directory_record> children; // Child records
116};
117
121struct options {
122 command_type command{command_type::none};
123 std::filesystem::path input_path;
124 std::filesystem::path output_path{"DICOMDIR"};
125 std::string file_set_id;
126 bool recursive{true};
127 bool verbose{false};
128 bool tree_format{true};
129 bool long_format{false};
130 bool check_files{false};
131 bool check_consistency{false};
132 std::vector<std::filesystem::path> add_paths;
133 std::vector<std::string> delete_paths;
134};
135
139struct statistics {
140 size_t total_files{0};
141 size_t valid_files{0};
142 size_t invalid_files{0};
143 size_t patients{0};
144 size_t studies{0};
145 size_t series{0};
146 size_t images{0};
147 std::vector<std::string> errors;
148 std::vector<std::string> warnings;
149};
150
151// ============================================================================
152// Utility Functions
153// ============================================================================
154
158void print_usage(const char* program_name) {
159 std::cout << "\nDICOM Directory (DICOMDIR) Utility\n\n";
160 std::cout << "Usage: " << program_name << " <command> [options] <arguments>\n\n";
161 std::cout << "Commands:\n";
162 std::cout << " create Create new DICOMDIR from directory\n";
163 std::cout << " list Display DICOMDIR contents\n";
164 std::cout << " verify Validate DICOMDIR\n";
165 std::cout << " update Update existing DICOMDIR\n\n";
166
167 std::cout << "Create Command:\n";
168 std::cout << " " << program_name << " create [options] <source_directory>\n";
169 std::cout << " Options:\n";
170 std::cout << " -o, --output <file> Output file (default: DICOMDIR)\n";
171 std::cout << " --file-set-id <id> File-set ID\n";
172 std::cout << " -r, --recursive Recursively scan directory (default)\n";
173 std::cout << " --no-recursive Do not scan subdirectories\n";
174 std::cout << " -v, --verbose Verbose output\n\n";
175
176 std::cout << "List Command:\n";
177 std::cout << " " << program_name << " list [options] <DICOMDIR>\n";
178 std::cout << " Options:\n";
179 std::cout << " -l, --long Detailed output\n";
180 std::cout << " --tree Tree format output (default)\n";
181 std::cout << " --flat Flat list output\n\n";
182
183 std::cout << "Verify Command:\n";
184 std::cout << " " << program_name << " verify [options] <DICOMDIR>\n";
185 std::cout << " Options:\n";
186 std::cout << " --check-files Verify all referenced files exist\n";
187 std::cout << " --check-consistency Check DICOMDIR consistency\n\n";
188
189 std::cout << "Update Command:\n";
190 std::cout << " " << program_name << " update [options] <DICOMDIR>\n";
191 std::cout << " Options:\n";
192 std::cout << " -a, --add <file/dir> Add file or directory\n";
193 std::cout << " -d, --delete <path> Delete entry by Referenced File ID\n\n";
194
195 std::cout << "General Options:\n";
196 std::cout << " -h, --help Show this help message\n\n";
197
198 std::cout << "Examples:\n";
199 std::cout << " " << program_name << " create -o DICOMDIR ./patient_data/\n";
200 std::cout << " " << program_name << " list --tree DICOMDIR\n";
201 std::cout << " " << program_name << " verify --check-files DICOMDIR\n";
202 std::cout << " " << program_name << " update -a ./new_study/ DICOMDIR\n\n";
203
204 std::cout << "Exit Codes:\n";
205 std::cout << " 0 Success\n";
206 std::cout << " 1 Invalid arguments\n";
207 std::cout << " 2 Processing error\n";
208}
209
213bool parse_arguments(int argc, char* argv[], options& opts) {
214 if (argc < 2) {
215 return false;
216 }
217
218 // Parse command
219 std::string cmd = argv[1];
220 if (cmd == "create") {
221 opts.command = command_type::create;
222 } else if (cmd == "list") {
223 opts.command = command_type::list;
224 } else if (cmd == "verify") {
225 opts.command = command_type::verify;
226 } else if (cmd == "update") {
227 opts.command = command_type::update;
228 } else if (cmd == "-h" || cmd == "--help") {
229 return false;
230 } else {
231 std::cerr << "Error: Unknown command '" << cmd << "'\n";
232 return false;
233 }
234
235 // Parse options
236 for (int i = 2; i < argc; ++i) {
237 std::string arg = argv[i];
238
239 if (arg == "-h" || arg == "--help") {
240 return false;
241 } else if ((arg == "-o" || arg == "--output") && i + 1 < argc) {
242 opts.output_path = argv[++i];
243 } else if (arg == "--file-set-id" && i + 1 < argc) {
244 opts.file_set_id = argv[++i];
245 } else if (arg == "-r" || arg == "--recursive") {
246 opts.recursive = true;
247 } else if (arg == "--no-recursive") {
248 opts.recursive = false;
249 } else if (arg == "-v" || arg == "--verbose") {
250 opts.verbose = true;
251 } else if (arg == "-l" || arg == "--long") {
252 opts.long_format = true;
253 } else if (arg == "--tree") {
254 opts.tree_format = true;
255 } else if (arg == "--flat") {
256 opts.tree_format = false;
257 } else if (arg == "--check-files") {
258 opts.check_files = true;
259 } else if (arg == "--check-consistency") {
260 opts.check_consistency = true;
261 } else if ((arg == "-a" || arg == "--add") && i + 1 < argc) {
262 opts.add_paths.emplace_back(argv[++i]);
263 } else if ((arg == "-d" || arg == "--delete") && i + 1 < argc) {
264 opts.delete_paths.emplace_back(argv[++i]);
265 } else if (arg[0] == '-') {
266 std::cerr << "Error: Unknown option '" << arg << "'\n";
267 return false;
268 } else {
269 if (opts.input_path.empty()) {
270 opts.input_path = arg;
271 } else {
272 std::cerr << "Error: Multiple input paths specified\n";
273 return false;
274 }
275 }
276 }
277
278 // Validate
279 if (opts.input_path.empty()) {
280 std::cerr << "Error: No input path specified\n";
281 return false;
282 }
283
284 return true;
285}
286
290std::string generate_uid() {
291 static uint64_t counter = 0;
292 auto now = std::chrono::system_clock::now();
293 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
294 now.time_since_epoch())
295 .count();
296 return std::string("1.2.826.0.1.3680043.8.1055.3.") + std::to_string(timestamp) +
297 "." + std::to_string(++counter);
298}
299
303std::string path_to_file_id(const std::filesystem::path& path,
304 const std::filesystem::path& base) {
305 auto relative = std::filesystem::relative(path, base);
306 std::string result;
307 for (const auto& part : relative) {
308 if (!result.empty()) {
309 result += "\\";
310 }
311 std::string name = part.string();
312 // Convert to uppercase for ISO 9660 compatibility
313 std::transform(name.begin(), name.end(), name.begin(), ::toupper);
314 result += name;
315 }
316 return result;
317}
318
319// ============================================================================
320// DICOM File Processing
321// ============================================================================
322
326struct patient_info {
327 std::string patient_id;
328 std::string patient_name;
329 std::map<std::string, struct study_info> studies;
330};
331
332struct study_info {
333 std::string study_instance_uid;
334 std::string study_date;
335 std::string study_time;
336 std::string study_description;
337 std::string accession_number;
338 std::map<std::string, struct series_info> series;
339};
340
341struct series_info {
342 std::string series_instance_uid;
343 std::string modality;
344 std::string series_number;
345 std::string series_description;
346 std::vector<struct instance_info> instances;
347};
348
349struct instance_info {
350 std::string sop_instance_uid;
351 std::string sop_class_uid;
352 std::string transfer_syntax_uid;
353 std::string instance_number;
354 std::filesystem::path file_path;
355};
356
360bool scan_directory(const std::filesystem::path& dir_path,
361 const std::filesystem::path& base_path,
362 std::map<std::string, patient_info>& patients,
363 const options& opts, statistics& stats) {
364 using namespace kcenon::pacs::core;
365
366 auto process_file = [&](const std::filesystem::path& file_path) {
367 ++stats.total_files;
368
369 // Check extension
370 auto ext = file_path.extension().string();
371 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
372 if (!ext.empty() && ext != ".dcm" && ext != ".dicom") {
373 return;
374 }
375
376 // Open DICOM file
377 auto result = dicom_file::open(file_path);
378 if (result.is_err()) {
379 ++stats.invalid_files;
380 if (opts.verbose) {
381 std::cerr << " Skip: " << file_path.filename().string()
382 << " (" << result.error().message << ")\n";
383 }
384 return;
385 }
386
387 ++stats.valid_files;
388 auto& file = result.value();
389 auto& ds = file.dataset();
390
391 // Extract patient info
392 std::string patient_id = ds.get_string(tags::patient_id);
393 if (patient_id.empty()) {
394 patient_id = "UNKNOWN";
395 }
396 std::string patient_name = ds.get_string(tags::patient_name);
397
398 // Extract study info
399 std::string study_uid = ds.get_string(tags::study_instance_uid);
400 if (study_uid.empty()) {
401 study_uid = generate_uid();
402 }
403
404 // Extract series info
405 std::string series_uid = ds.get_string(tags::series_instance_uid);
406 if (series_uid.empty()) {
407 series_uid = generate_uid();
408 }
409
410 // Build hierarchy
411 auto& patient = patients[patient_id];
412 patient.patient_id = patient_id;
413 if (patient.patient_name.empty()) {
414 patient.patient_name = patient_name;
415 }
416
417 auto& study = patient.studies[study_uid];
418 study.study_instance_uid = study_uid;
419 if (study.study_date.empty()) {
420 study.study_date = ds.get_string(tags::study_date);
421 study.study_time = ds.get_string(tags::study_time);
422 study.study_description = ds.get_string(tags::study_description);
423 study.accession_number = ds.get_string(tags::accession_number);
424 }
425
426 auto& series = study.series[series_uid];
427 series.series_instance_uid = series_uid;
428 if (series.modality.empty()) {
429 series.modality = ds.get_string(tags::modality);
430 series.series_number = ds.get_string(tags::series_number);
431 series.series_description = ds.get_string(tags::series_description);
432 }
433
434 // Add instance
435 instance_info instance;
436 instance.sop_instance_uid = file.sop_instance_uid();
437 instance.sop_class_uid = file.sop_class_uid();
438 instance.transfer_syntax_uid = file.transfer_syntax().uid();
439 instance.instance_number = ds.get_string(tags::instance_number);
440 instance.file_path = file_path;
441 series.instances.push_back(std::move(instance));
442
443 if (opts.verbose) {
444 std::cout << " Add: " << file_path.filename().string() << "\n";
445 }
446 };
447
448 // Iterate directory
449 try {
450 if (opts.recursive) {
451 for (const auto& entry :
452 std::filesystem::recursive_directory_iterator(dir_path)) {
453 if (entry.is_regular_file()) {
454 process_file(entry.path());
455 }
456 }
457 } else {
458 for (const auto& entry : std::filesystem::directory_iterator(dir_path)) {
459 if (entry.is_regular_file()) {
460 process_file(entry.path());
461 }
462 }
463 }
464 } catch (const std::filesystem::filesystem_error& e) {
465 std::cerr << "Error: " << e.what() << "\n";
466 return false;
467 }
468
469 // Count statistics
470 stats.patients = patients.size();
471 for (const auto& [pid, patient] : patients) {
472 stats.studies += patient.studies.size();
473 for (const auto& [suid, study] : patient.studies) {
474 stats.series += study.series.size();
475 for (const auto& [seuid, series] : study.series) {
476 stats.images += series.instances.size();
477 }
478 }
479 }
480
481 return true;
482}
483
484// ============================================================================
485// DICOMDIR Creation
486// ============================================================================
487
491kcenon::pacs::core::dicom_dataset create_dicomdir_dataset(
492 const std::map<std::string, patient_info>& patients,
493 const std::filesystem::path& base_path,
494 const options& opts) {
495 using namespace kcenon::pacs::core;
496 using namespace kcenon::pacs::encoding;
497
498 dicom_dataset ds;
499
500 // Set basic DICOMDIR attributes
501 ds.set_string(dir_tags::file_set_id, vr_type::CS,
502 opts.file_set_id.empty() ? "PACS_SYSTEM" : opts.file_set_id);
503 ds.set_numeric<uint16_t>(dir_tags::file_set_consistency_flag, vr_type::US, 0);
504
505 // Create directory record sequence
506 std::vector<dicom_dataset> records;
507
508 for (const auto& [pid, patient] : patients) {
509 // Create PATIENT record
510 dicom_dataset patient_rec;
511 patient_rec.set_string(dir_tags::directory_record_type, vr_type::CS, "PATIENT");
512 patient_rec.set_numeric<uint16_t>(dir_tags::record_in_use_flag, vr_type::US, 0xFFFF);
513 patient_rec.set_string(tags::patient_id, vr_type::LO, patient.patient_id);
514 patient_rec.set_string(tags::patient_name, vr_type::PN, patient.patient_name);
515
516 for (const auto& [suid, study] : patient.studies) {
517 // Create STUDY record
518 dicom_dataset study_rec;
519 study_rec.set_string(dir_tags::directory_record_type, vr_type::CS, "STUDY");
520 study_rec.set_numeric<uint16_t>(dir_tags::record_in_use_flag, vr_type::US, 0xFFFF);
521 study_rec.set_string(tags::study_instance_uid, vr_type::UI, study.study_instance_uid);
522 study_rec.set_string(tags::study_date, vr_type::DA, study.study_date);
523 study_rec.set_string(tags::study_time, vr_type::TM, study.study_time);
524 study_rec.set_string(tags::study_description, vr_type::LO, study.study_description);
525 study_rec.set_string(tags::accession_number, vr_type::SH, study.accession_number);
526 study_rec.set_string(tags::study_id, vr_type::SH, "");
527
528 for (const auto& [seuid, series] : study.series) {
529 // Create SERIES record
530 dicom_dataset series_rec;
531 series_rec.set_string(dir_tags::directory_record_type, vr_type::CS, "SERIES");
532 series_rec.set_numeric<uint16_t>(dir_tags::record_in_use_flag, vr_type::US, 0xFFFF);
533 series_rec.set_string(tags::series_instance_uid, vr_type::UI, series.series_instance_uid);
534 series_rec.set_string(tags::modality, vr_type::CS, series.modality);
535 series_rec.set_string(tags::series_number, vr_type::IS, series.series_number);
536
537 for (const auto& instance : series.instances) {
538 // Create IMAGE record
539 dicom_dataset image_rec;
540 image_rec.set_string(dir_tags::directory_record_type, vr_type::CS, "IMAGE");
541 image_rec.set_numeric<uint16_t>(dir_tags::record_in_use_flag, vr_type::US, 0xFFFF);
542
543 // Referenced File ID
544 std::string file_id = path_to_file_id(instance.file_path, base_path);
545 image_rec.set_string(dir_tags::referenced_file_id, vr_type::CS, file_id);
546
547 // Referenced SOP info
548 image_rec.set_string(dir_tags::referenced_sop_class_uid_in_file,
549 vr_type::UI, instance.sop_class_uid);
550 image_rec.set_string(dir_tags::referenced_sop_instance_uid_in_file,
551 vr_type::UI, instance.sop_instance_uid);
552 image_rec.set_string(dir_tags::referenced_transfer_syntax_uid_in_file,
553 vr_type::UI, instance.transfer_syntax_uid);
554
555 // Instance Number
556 image_rec.set_string(tags::instance_number, vr_type::IS, instance.instance_number);
557
558 records.push_back(std::move(image_rec));
559 }
560 records.push_back(std::move(series_rec));
561 }
562 records.push_back(std::move(study_rec));
563 }
564 records.push_back(std::move(patient_rec));
565 }
566
567 // Reverse records (DICOMDIR uses bottom-up order for linking)
568 std::reverse(records.begin(), records.end());
569
570 // Create sequence element and set items
571 dicom_element seq_elem(dir_tags::directory_record_sequence, vr_type::SQ);
572 auto& items = seq_elem.sequence_items();
573 items = std::move(records);
574 ds.insert(std::move(seq_elem));
575
576 // Set SOP Class and Instance UIDs
577 ds.set_string(tags::sop_class_uid, vr_type::UI, kMediaStorageDirectorySopClassUid);
578 ds.set_string(tags::sop_instance_uid, vr_type::UI, generate_uid());
579
580 return ds;
581}
582
586int execute_create(const options& opts) {
587 using namespace kcenon::pacs::core;
588 using namespace kcenon::pacs::encoding;
589
590 std::cout << "Creating DICOMDIR from: " << opts.input_path.string() << "\n";
591
592 if (!std::filesystem::exists(opts.input_path)) {
593 std::cerr << "Error: Source directory does not exist\n";
594 return 2;
595 }
596
597 if (!std::filesystem::is_directory(opts.input_path)) {
598 std::cerr << "Error: Source path is not a directory\n";
599 return 2;
600 }
601
602 // Scan directory
603 std::map<std::string, patient_info> patients;
604 statistics stats;
605
606 std::cout << "Scanning directory...\n";
607 if (!scan_directory(opts.input_path, opts.input_path, patients, opts, stats)) {
608 return 2;
609 }
610
611 if (stats.valid_files == 0) {
612 std::cerr << "Error: No valid DICOM files found\n";
613 return 2;
614 }
615
616 // Create DICOMDIR dataset
617 std::cout << "Building DICOMDIR structure...\n";
618 auto ds = create_dicomdir_dataset(patients, opts.input_path, opts);
619
620 // Create DICOM file
621 auto file = dicom_file::create(std::move(ds),
622 transfer_syntax::explicit_vr_little_endian);
623
624 // Determine output path
625 std::filesystem::path output_path = opts.output_path;
626 if (output_path.is_relative()) {
627 output_path = opts.input_path / output_path;
628 }
629
630 // Save
631 std::cout << "Saving to: " << output_path.string() << "\n";
632 auto result = file.save(output_path);
633 if (result.is_err()) {
634 std::cerr << "Error: Failed to save DICOMDIR: " << result.error().message << "\n";
635 return 2;
636 }
637
638 // Print summary
639 std::cout << "\n";
640 std::cout << "========================================\n";
641 std::cout << " DICOMDIR Created\n";
642 std::cout << "========================================\n";
643 std::cout << " Total files scanned: " << stats.total_files << "\n";
644 std::cout << " Valid DICOM files: " << stats.valid_files << "\n";
645 std::cout << " Invalid/Skipped: " << stats.invalid_files << "\n";
646 std::cout << " --------------------------------\n";
647 std::cout << " Patients: " << stats.patients << "\n";
648 std::cout << " Studies: " << stats.studies << "\n";
649 std::cout << " Series: " << stats.series << "\n";
650 std::cout << " Images: " << stats.images << "\n";
651 std::cout << "========================================\n";
652
653 return 0;
654}
655
656// ============================================================================
657// DICOMDIR Listing
658// ============================================================================
659
663bool parse_dicomdir(const std::filesystem::path& dicomdir_path,
664 std::vector<directory_record>& root_records,
665 statistics& stats) {
666 using namespace kcenon::pacs::core;
667
668 auto result = dicom_file::open(dicomdir_path);
669 if (result.is_err()) {
670 std::cerr << "Error: Failed to open DICOMDIR: " << result.error().message << "\n";
671 return false;
672 }
673
674 auto& file = result.value();
675 auto& ds = file.dataset();
676
677 // Verify SOP Class
678 auto sop_class = ds.get_string(tags::sop_class_uid);
679 if (sop_class != kMediaStorageDirectorySopClassUid) {
680 std::cerr << "Warning: Not a standard DICOMDIR (SOP Class: " << sop_class << ")\n";
681 }
682
683 // Get Directory Record Sequence
684 auto* seq_elem = ds.get(dir_tags::directory_record_sequence);
685 if (seq_elem == nullptr || !seq_elem->is_sequence()) {
686 std::cerr << "Error: No Directory Record Sequence found\n";
687 return false;
688 }
689
690 const auto& items = seq_elem->sequence_items();
691
692 // Build hierarchy using a stack-based approach
693 std::vector<directory_record*> stack;
694
695 for (const auto& item : items) {
696 directory_record rec;
697 rec.type = item.get_string(dir_tags::directory_record_type);
698
699 // Extract type-specific attributes
700 if (rec.type == "PATIENT") {
701 rec.attrs["PatientID"] = item.get_string(tags::patient_id);
702 rec.attrs["PatientName"] = item.get_string(tags::patient_name);
703 ++stats.patients;
704 } else if (rec.type == "STUDY") {
705 rec.attrs["StudyInstanceUID"] = item.get_string(tags::study_instance_uid);
706 rec.attrs["StudyDate"] = item.get_string(tags::study_date);
707 rec.attrs["StudyDescription"] = item.get_string(tags::study_description);
708 rec.attrs["AccessionNumber"] = item.get_string(tags::accession_number);
709 ++stats.studies;
710 } else if (rec.type == "SERIES") {
711 rec.attrs["SeriesInstanceUID"] = item.get_string(tags::series_instance_uid);
712 rec.attrs["Modality"] = item.get_string(tags::modality);
713 rec.attrs["SeriesNumber"] = item.get_string(tags::series_number);
714 ++stats.series;
715 } else if (rec.type == "IMAGE") {
716 rec.attrs["InstanceNumber"] = item.get_string(tags::instance_number);
717 rec.file_path = item.get_string(dir_tags::referenced_file_id);
718 rec.sop_class_uid = item.get_string(dir_tags::referenced_sop_class_uid_in_file);
719 rec.sop_instance_uid = item.get_string(dir_tags::referenced_sop_instance_uid_in_file);
720 rec.transfer_syntax_uid = item.get_string(dir_tags::referenced_transfer_syntax_uid_in_file);
721 ++stats.images;
722 }
723
724 // Determine hierarchy level
725 int level = 0;
726 if (rec.type == "PATIENT") level = 0;
727 else if (rec.type == "STUDY") level = 1;
728 else if (rec.type == "SERIES") level = 2;
729 else if (rec.type == "IMAGE") level = 3;
730 else level = 4; // Unknown type
731
732 // Adjust stack
733 while (stack.size() > static_cast<size_t>(level)) {
734 stack.pop_back();
735 }
736
737 // Add to appropriate parent
738 if (stack.empty()) {
739 root_records.push_back(std::move(rec));
740 stack.push_back(&root_records.back());
741 } else {
742 stack.back()->children.push_back(std::move(rec));
743 stack.push_back(&stack.back()->children.back());
744 }
745 }
746
747 return true;
748}
749
753void print_record_tree(const directory_record& rec, int depth, const options& opts) {
754 std::string indent(depth * 2, ' ');
755 std::string prefix = depth == 0 ? "" : "├── ";
756
757 if (rec.type == "PATIENT") {
758 std::cout << indent << prefix << "[PATIENT] "
759 << rec.attrs.at("PatientName") << " (" << rec.attrs.at("PatientID") << ")\n";
760 } else if (rec.type == "STUDY") {
761 std::cout << indent << prefix << "[STUDY] "
762 << rec.attrs.at("StudyDate") << " "
763 << rec.attrs.at("StudyDescription") << "\n";
764 if (opts.long_format) {
765 std::cout << indent << " UID: " << rec.attrs.at("StudyInstanceUID") << "\n";
766 std::cout << indent << " Accession: " << rec.attrs.at("AccessionNumber") << "\n";
767 }
768 } else if (rec.type == "SERIES") {
769 std::cout << indent << prefix << "[SERIES] "
770 << rec.attrs.at("Modality") << " #" << rec.attrs.at("SeriesNumber") << "\n";
771 if (opts.long_format) {
772 std::cout << indent << " UID: " << rec.attrs.at("SeriesInstanceUID") << "\n";
773 }
774 } else if (rec.type == "IMAGE") {
775 std::cout << indent << prefix << "[IMAGE] #" << rec.attrs.at("InstanceNumber");
776 if (!rec.file_path.empty()) {
777 std::cout << " -> " << rec.file_path.string();
778 }
779 std::cout << "\n";
780 if (opts.long_format) {
781 std::cout << indent << " SOP: " << rec.sop_class_uid << "\n";
782 }
783 } else {
784 std::cout << indent << prefix << "[" << rec.type << "]\n";
785 }
786
787 // Print children
788 for (const auto& child : rec.children) {
789 print_record_tree(child, depth + 1, opts);
790 }
791}
792
796int execute_list(const options& opts) {
797 std::cout << "DICOMDIR: " << opts.input_path.string() << "\n\n";
798
799 if (!std::filesystem::exists(opts.input_path)) {
800 std::cerr << "Error: DICOMDIR file does not exist\n";
801 return 2;
802 }
803
804 std::vector<directory_record> root_records;
805 statistics stats;
806
807 if (!parse_dicomdir(opts.input_path, root_records, stats)) {
808 return 2;
809 }
810
811 // Print tree
812 if (opts.tree_format) {
813 for (const auto& rec : root_records) {
814 print_record_tree(rec, 0, opts);
815 }
816 } else {
817 // Flat format
818 std::function<void(const directory_record&)> print_flat;
819 print_flat = [&](const directory_record& rec) {
820 if (rec.type == "IMAGE" && !rec.file_path.empty()) {
821 std::cout << rec.file_path.string() << "\n";
822 }
823 for (const auto& child : rec.children) {
824 print_flat(child);
825 }
826 };
827 for (const auto& rec : root_records) {
828 print_flat(rec);
829 }
830 }
831
832 // Print summary
833 std::cout << "\n";
834 std::cout << "----------------------------------------\n";
835 std::cout << " Patients: " << stats.patients << "\n";
836 std::cout << " Studies: " << stats.studies << "\n";
837 std::cout << " Series: " << stats.series << "\n";
838 std::cout << " Images: " << stats.images << "\n";
839 std::cout << "----------------------------------------\n";
840
841 return 0;
842}
843
844// ============================================================================
845// DICOMDIR Verification
846// ============================================================================
847
851void verify_files(const std::vector<directory_record>& records,
852 const std::filesystem::path& base_path,
853 statistics& stats) {
854 std::function<void(const directory_record&)> check;
855 check = [&](const directory_record& rec) {
856 if (rec.type == "IMAGE" && !rec.file_path.empty()) {
857 // Convert Referenced File ID to filesystem path
858 std::string file_id = rec.file_path.string();
859 std::replace(file_id.begin(), file_id.end(), '\\', '/');
860 std::filesystem::path full_path = base_path / file_id;
861
862 if (!std::filesystem::exists(full_path)) {
863 stats.errors.push_back("Missing file: " + full_path.string());
864 } else {
865 ++stats.valid_files;
866 }
867 ++stats.total_files;
868 }
869
870 for (const auto& child : rec.children) {
871 check(child);
872 }
873 };
874
875 for (const auto& rec : records) {
876 check(rec);
877 }
878}
879
883int execute_verify(const options& opts) {
884 std::cout << "Verifying DICOMDIR: " << opts.input_path.string() << "\n\n";
885
886 if (!std::filesystem::exists(opts.input_path)) {
887 std::cerr << "Error: DICOMDIR file does not exist\n";
888 return 2;
889 }
890
891 std::vector<directory_record> root_records;
892 statistics stats;
893
894 // Parse DICOMDIR
895 std::cout << "Parsing DICOMDIR...\n";
896 if (!parse_dicomdir(opts.input_path, root_records, stats)) {
897 return 2;
898 }
899 std::cout << " Found " << stats.images << " image records\n";
900
901 // Check files if requested
902 if (opts.check_files) {
903 std::cout << "\nVerifying referenced files...\n";
904 std::filesystem::path base_path = opts.input_path.parent_path();
905 verify_files(root_records, base_path, stats);
906
907 std::cout << " Files found: " << stats.valid_files << "/" << stats.total_files << "\n";
908 stats.invalid_files = stats.total_files - stats.valid_files;
909 }
910
911 // Check consistency if requested
912 if (opts.check_consistency) {
913 std::cout << "\nChecking consistency...\n";
914
915 // Check for duplicate SOP Instance UIDs
916 std::set<std::string> sop_uids;
917 std::function<void(const directory_record&)> check_duplicates;
918 check_duplicates = [&](const directory_record& rec) {
919 if (rec.type == "IMAGE" && !rec.sop_instance_uid.empty()) {
920 if (sop_uids.count(rec.sop_instance_uid) > 0) {
921 stats.warnings.push_back("Duplicate SOP Instance UID: " + rec.sop_instance_uid);
922 } else {
923 sop_uids.insert(rec.sop_instance_uid);
924 }
925 }
926 for (const auto& child : rec.children) {
927 check_duplicates(child);
928 }
929 };
930
931 for (const auto& rec : root_records) {
932 check_duplicates(rec);
933 }
934
935 std::cout << " Unique SOP Instance UIDs: " << sop_uids.size() << "\n";
936 }
937
938 // Print results
939 std::cout << "\n";
940 std::cout << "========================================\n";
941 std::cout << " Verification Results\n";
942 std::cout << "========================================\n";
943 std::cout << " Patients: " << stats.patients << "\n";
944 std::cout << " Studies: " << stats.studies << "\n";
945 std::cout << " Series: " << stats.series << "\n";
946 std::cout << " Images: " << stats.images << "\n";
947
948 if (opts.check_files) {
949 std::cout << " --------------------------------\n";
950 std::cout << " Files verified: " << stats.valid_files << "/" << stats.total_files << "\n";
951 if (stats.invalid_files > 0) {
952 std::cout << " Missing files: " << stats.invalid_files << "\n";
953 }
954 }
955
956 // Print errors
957 if (!stats.errors.empty()) {
958 std::cout << " --------------------------------\n";
959 std::cout << " Errors: " << stats.errors.size() << "\n";
960 for (const auto& err : stats.errors) {
961 std::cout << " - " << err << "\n";
962 }
963 }
964
965 // Print warnings
966 if (!stats.warnings.empty()) {
967 std::cout << " --------------------------------\n";
968 std::cout << " Warnings: " << stats.warnings.size() << "\n";
969 for (const auto& warn : stats.warnings) {
970 std::cout << " - " << warn << "\n";
971 }
972 }
973
974 std::cout << "========================================\n";
975
976 bool success = stats.errors.empty() &&
977 (stats.invalid_files == 0 || !opts.check_files);
978 std::cout << "\nResult: " << (success ? "PASSED" : "FAILED") << "\n";
979
980 return success ? 0 : 2;
981}
982
983// ============================================================================
984// DICOMDIR Update
985// ============================================================================
986
990int execute_update(const options& opts) {
991 std::cout << "Updating DICOMDIR: " << opts.input_path.string() << "\n\n";
992
993 if (!std::filesystem::exists(opts.input_path)) {
994 std::cerr << "Error: DICOMDIR file does not exist\n";
995 return 2;
996 }
997
998 if (opts.add_paths.empty() && opts.delete_paths.empty()) {
999 std::cerr << "Error: No add or delete operations specified\n";
1000 return 1;
1001 }
1002
1003 // Parse existing DICOMDIR
1004 std::vector<directory_record> root_records;
1005 statistics stats;
1006
1007 if (!parse_dicomdir(opts.input_path, root_records, stats)) {
1008 return 2;
1009 }
1010
1011 std::filesystem::path base_path = opts.input_path.parent_path();
1012
1013 // Handle add operations
1014 if (!opts.add_paths.empty()) {
1015 std::map<std::string, patient_info> patients;
1016
1017 // First, rebuild patient map from existing records
1018 std::function<void(const directory_record&, patient_info*, study_info*, series_info*)> rebuild;
1019 rebuild = [&](const directory_record& rec, patient_info* patient,
1020 study_info* study, series_info* series) {
1021 if (rec.type == "PATIENT") {
1022 auto& p = patients[rec.attrs.at("PatientID")];
1023 p.patient_id = rec.attrs.at("PatientID");
1024 p.patient_name = rec.attrs.at("PatientName");
1025 for (const auto& child : rec.children) {
1026 rebuild(child, &p, nullptr, nullptr);
1027 }
1028 } else if (rec.type == "STUDY" && patient != nullptr) {
1029 auto& s = patient->studies[rec.attrs.at("StudyInstanceUID")];
1030 s.study_instance_uid = rec.attrs.at("StudyInstanceUID");
1031 s.study_date = rec.attrs.at("StudyDate");
1032 s.study_description = rec.attrs.at("StudyDescription");
1033 s.accession_number = rec.attrs.at("AccessionNumber");
1034 for (const auto& child : rec.children) {
1035 rebuild(child, patient, &s, nullptr);
1036 }
1037 } else if (rec.type == "SERIES" && study != nullptr) {
1038 auto& se = study->series[rec.attrs.at("SeriesInstanceUID")];
1039 se.series_instance_uid = rec.attrs.at("SeriesInstanceUID");
1040 se.modality = rec.attrs.at("Modality");
1041 se.series_number = rec.attrs.at("SeriesNumber");
1042 for (const auto& child : rec.children) {
1043 rebuild(child, patient, study, &se);
1044 }
1045 } else if (rec.type == "IMAGE" && series != nullptr) {
1046 instance_info inst;
1047 inst.sop_instance_uid = rec.sop_instance_uid;
1048 inst.sop_class_uid = rec.sop_class_uid;
1049 inst.transfer_syntax_uid = rec.transfer_syntax_uid;
1050 inst.instance_number = rec.attrs.at("InstanceNumber");
1051
1052 // Convert file ID to path
1053 std::string file_id = rec.file_path.string();
1054 std::replace(file_id.begin(), file_id.end(), '\\', '/');
1055 inst.file_path = base_path / file_id;
1056
1057 series->instances.push_back(std::move(inst));
1058 }
1059 };
1060
1061 for (const auto& rec : root_records) {
1062 rebuild(rec, nullptr, nullptr, nullptr);
1063 }
1064
1065 // Add new files
1066 for (const auto& add_path : opts.add_paths) {
1067 std::cout << "Adding: " << add_path.string() << "\n";
1068 options scan_opts = opts;
1069 scan_opts.verbose = true;
1070
1071 if (std::filesystem::is_directory(add_path)) {
1072 scan_directory(add_path, base_path, patients, scan_opts, stats);
1073 } else if (std::filesystem::is_regular_file(add_path)) {
1074 // Handle single file - create a temporary directory iterator
1075 std::map<std::string, patient_info> temp_patients;
1076 options single_opts = scan_opts;
1077 single_opts.recursive = false;
1078 scan_directory(add_path.parent_path(), base_path, temp_patients, single_opts, stats);
1079 // Merge into main patients
1080 for (auto& [pid, patient] : temp_patients) {
1081 auto& p = patients[pid];
1082 if (p.patient_id.empty()) {
1083 p = std::move(patient);
1084 } else {
1085 for (auto& [suid, study] : patient.studies) {
1086 if (p.studies.count(suid) == 0) {
1087 p.studies[suid] = std::move(study);
1088 } else {
1089 for (auto& [seuid, series] : study.series) {
1090 if (p.studies[suid].series.count(seuid) == 0) {
1091 p.studies[suid].series[seuid] = std::move(series);
1092 } else {
1093 for (auto& inst : series.instances) {
1094 p.studies[suid].series[seuid].instances.push_back(std::move(inst));
1095 }
1096 }
1097 }
1098 }
1099 }
1100 }
1101 }
1102 }
1103 }
1104
1105 // Recreate DICOMDIR
1106 std::cout << "\nRebuilding DICOMDIR...\n";
1107 auto ds = create_dicomdir_dataset(patients, base_path, opts);
1108
1111
1112 auto result = file.save(opts.input_path);
1113 if (result.is_err()) {
1114 std::cerr << "Error: Failed to save updated DICOMDIR: "
1115 << result.error().message << "\n";
1116 return 2;
1117 }
1118 }
1119
1120 // Handle delete operations
1121 if (!opts.delete_paths.empty()) {
1122 std::cout << "Delete operation not yet implemented\n";
1123 return 1;
1124 }
1125
1126 std::cout << "\nDICOMDIR updated successfully.\n";
1127 return 0;
1128}
1129
1130} // namespace
1131
1132// ============================================================================
1133// Main
1134// ============================================================================
1135
1136int main(int argc, char* argv[]) {
1137 std::cout << R"(
1138 ____ ____ __ __ ____ ___ ____
1139 | _ \ / ___| \/ | | _ \_ _| _ \
1140 | | | | | | |\/| | | | | | || |_) |
1141 | |_| | |___| | | | | |_| | || _ <
1142 |____/ \____|_| |_| |____/___|_| \_\
1143
1144 DICOMDIR Creation/Management Utility
1145)" << "\n";
1146
1147 options opts;
1148
1149 if (!parse_arguments(argc, argv, opts)) {
1150 print_usage(argv[0]);
1151 return 1;
1152 }
1153
1154 switch (opts.command) {
1155 case command_type::create:
1156 return execute_create(opts);
1157 case command_type::list:
1158 return execute_list(opts);
1159 case command_type::verify:
1160 return execute_verify(opts);
1161 case command_type::update:
1162 return execute_update(opts);
1163 default:
1164 print_usage(argv[0]);
1165 return 1;
1166 }
1167}
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 Data Dictionary for tag metadata lookup.
DICOM Part 10 file handling for reading/writing DICOM files.
Compile-time constants for commonly used DICOM tags.
int main()
Definition main.cpp:84
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag item
Item.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag series_description
Series Description.
constexpr dicom_tag transfer_syntax_uid
Transfer Syntax UID.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr dicom_tag instance_number
Instance Number.
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
@ counter
Monotonic increasing value.
std::string_view name