88constexpr const char* kMediaStorageDirectorySopClassUid =
"1.2.840.10008.1.3.10";
91constexpr const char* kImplementationClassUid =
"1.2.826.0.1.3680043.8.1055.1";
94constexpr const char* kImplementationVersionName =
"PACS_SYS_001";
103enum class command_type { none, create, list, verify, update };
108struct directory_record {
110 std::map<std::string, std::string> attrs;
111 std::filesystem::path file_path;
115 std::vector<directory_record> children;
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};
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;
140 size_t total_files{0};
141 size_t valid_files{0};
142 size_t invalid_files{0};
147 std::vector<std::string> errors;
148 std::vector<std::string> warnings;
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";
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";
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";
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";
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";
195 std::cout <<
"General Options:\n";
196 std::cout <<
" -h, --help Show this help message\n\n";
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";
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";
213bool parse_arguments(
int argc,
char* argv[], options& opts) {
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") {
231 std::cerr <<
"Error: Unknown command '" << cmd <<
"'\n";
236 for (
int i = 2; i < argc; ++i) {
237 std::string arg = argv[i];
239 if (arg ==
"-h" || arg ==
"--help") {
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") {
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";
269 if (opts.input_path.empty()) {
270 opts.input_path = arg;
272 std::cerr <<
"Error: Multiple input paths specified\n";
279 if (opts.input_path.empty()) {
280 std::cerr <<
"Error: No input path specified\n";
292 auto now = std::chrono::system_clock::now();
293 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
294 now.time_since_epoch())
296 return std::string(
"1.2.826.0.1.3680043.8.1055.3.") + std::to_string(timestamp) +
297 "." + std::to_string(++counter);
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);
307 for (
const auto& part : relative) {
308 if (!result.empty()) {
311 std::string
name = part.string();
313 std::transform(
name.begin(),
name.end(),
name.begin(), ::toupper);
329 std::map<std::string, struct study_info> studies;
338 std::map<std::string, struct series_info>
series;
346 std::vector<struct instance_info> instances;
349struct instance_info {
354 std::filesystem::path file_path;
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) {
366 auto process_file = [&](
const std::filesystem::path& file_path) {
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") {
377 auto result = dicom_file::open(file_path);
378 if (result.is_err()) {
379 ++stats.invalid_files;
381 std::cerr <<
" Skip: " << file_path.filename().string()
382 <<
" (" << result.error().message <<
")\n";
388 auto& file = result.value();
389 auto& ds = file.dataset();
392 std::string
patient_id = ds.get_string(tags::patient_id);
396 std::string
patient_name = ds.get_string(tags::patient_name);
399 std::string study_uid = ds.get_string(tags::study_instance_uid);
400 if (study_uid.empty()) {
405 std::string series_uid = ds.get_string(tags::series_instance_uid);
406 if (series_uid.empty()) {
413 if (
patient.patient_name.empty()) {
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);
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);
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));
444 std::cout <<
" Add: " << file_path.filename().string() <<
"\n";
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());
458 for (
const auto& entry : std::filesystem::directory_iterator(dir_path)) {
459 if (entry.is_regular_file()) {
460 process_file(entry.path());
464 }
catch (
const std::filesystem::filesystem_error& e) {
465 std::cerr <<
"Error: " << e.what() <<
"\n";
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();
492 const std::map<std::string, patient_info>& patients,
493 const std::filesystem::path& base_path,
494 const options& opts) {
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);
506 std::vector<dicom_dataset> records;
508 for (
const auto& [pid, patient] : patients) {
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);
516 for (
const auto& [suid, study] :
patient.studies) {
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,
"");
528 for (
const auto& [seuid, series] :
study.series) {
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);
537 for (
const auto& instance :
series.instances) {
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);
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);
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);
556 image_rec.set_string(tags::instance_number, vr_type::IS, instance.instance_number);
558 records.push_back(std::move(image_rec));
560 records.push_back(std::move(series_rec));
562 records.push_back(std::move(study_rec));
564 records.push_back(std::move(patient_rec));
568 std::reverse(records.begin(), records.end());
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));
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());
586int execute_create(
const options& opts) {
590 std::cout <<
"Creating DICOMDIR from: " << opts.input_path.string() <<
"\n";
592 if (!std::filesystem::exists(opts.input_path)) {
593 std::cerr <<
"Error: Source directory does not exist\n";
597 if (!std::filesystem::is_directory(opts.input_path)) {
598 std::cerr <<
"Error: Source path is not a directory\n";
603 std::map<std::string, patient_info> patients;
606 std::cout <<
"Scanning directory...\n";
607 if (!scan_directory(opts.input_path, opts.input_path, patients, opts, stats)) {
611 if (stats.valid_files == 0) {
612 std::cerr <<
"Error: No valid DICOM files found\n";
617 std::cout <<
"Building DICOMDIR structure...\n";
618 auto ds = create_dicomdir_dataset(patients, opts.input_path, opts);
621 auto file = dicom_file::create(std::move(ds),
622 transfer_syntax::explicit_vr_little_endian);
625 std::filesystem::path output_path = opts.output_path;
626 if (output_path.is_relative()) {
627 output_path = opts.input_path / output_path;
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";
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";
663bool parse_dicomdir(
const std::filesystem::path& dicomdir_path,
664 std::vector<directory_record>& root_records,
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";
674 auto& file = result.value();
675 auto& ds = file.dataset();
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";
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";
690 const auto& items = seq_elem->sequence_items();
693 std::vector<directory_record*> stack;
695 for (
const auto& item : items) {
696 directory_record rec;
697 rec.type =
item.get_string(dir_tags::directory_record_type);
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);
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);
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);
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);
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;
733 while (stack.size() >
static_cast<size_t>(level)) {
739 root_records.push_back(std::move(rec));
740 stack.push_back(&root_records.back());
742 stack.back()->children.push_back(std::move(rec));
743 stack.push_back(&stack.back()->children.back());
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 ?
"" :
"├── ";
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";
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";
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();
780 if (opts.long_format) {
781 std::cout << indent <<
" SOP: " << rec.sop_class_uid <<
"\n";
784 std::cout << indent << prefix <<
"[" << rec.type <<
"]\n";
788 for (
const auto& child : rec.children) {
789 print_record_tree(child, depth + 1, opts);
796int execute_list(
const options& opts) {
797 std::cout <<
"DICOMDIR: " << opts.input_path.string() <<
"\n\n";
799 if (!std::filesystem::exists(opts.input_path)) {
800 std::cerr <<
"Error: DICOMDIR file does not exist\n";
804 std::vector<directory_record> root_records;
807 if (!parse_dicomdir(opts.input_path, root_records, stats)) {
812 if (opts.tree_format) {
813 for (
const auto& rec : root_records) {
814 print_record_tree(rec, 0, opts);
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";
823 for (
const auto& child : rec.children) {
827 for (
const auto& rec : root_records) {
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";
851void verify_files(
const std::vector<directory_record>& records,
852 const std::filesystem::path& base_path,
854 std::function<void(
const directory_record&)> check;
855 check = [&](
const directory_record& rec) {
856 if (rec.type ==
"IMAGE" && !rec.file_path.empty()) {
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;
862 if (!std::filesystem::exists(full_path)) {
863 stats.errors.push_back(
"Missing file: " + full_path.string());
870 for (
const auto& child : rec.children) {
875 for (
const auto& rec : records) {
883int execute_verify(
const options& opts) {
884 std::cout <<
"Verifying DICOMDIR: " << opts.input_path.string() <<
"\n\n";
886 if (!std::filesystem::exists(opts.input_path)) {
887 std::cerr <<
"Error: DICOMDIR file does not exist\n";
891 std::vector<directory_record> root_records;
895 std::cout <<
"Parsing DICOMDIR...\n";
896 if (!parse_dicomdir(opts.input_path, root_records, stats)) {
899 std::cout <<
" Found " << stats.images <<
" image records\n";
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);
907 std::cout <<
" Files found: " << stats.valid_files <<
"/" << stats.total_files <<
"\n";
908 stats.invalid_files = stats.total_files - stats.valid_files;
912 if (opts.check_consistency) {
913 std::cout <<
"\nChecking consistency...\n";
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);
923 sop_uids.insert(rec.sop_instance_uid);
926 for (
const auto& child : rec.children) {
927 check_duplicates(child);
931 for (
const auto& rec : root_records) {
932 check_duplicates(rec);
935 std::cout <<
" Unique SOP Instance UIDs: " << sop_uids.size() <<
"\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";
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";
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";
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";
974 std::cout <<
"========================================\n";
976 bool success = stats.errors.empty() &&
977 (stats.invalid_files == 0 || !opts.check_files);
978 std::cout <<
"\nResult: " << (
success ?
"PASSED" :
"FAILED") <<
"\n";
990int execute_update(
const options& opts) {
991 std::cout <<
"Updating DICOMDIR: " << opts.input_path.string() <<
"\n\n";
993 if (!std::filesystem::exists(opts.input_path)) {
994 std::cerr <<
"Error: DICOMDIR file does not exist\n";
998 if (opts.add_paths.empty() && opts.delete_paths.empty()) {
999 std::cerr <<
"Error: No add or delete operations specified\n";
1004 std::vector<directory_record> root_records;
1007 if (!parse_dicomdir(opts.input_path, root_records, stats)) {
1011 std::filesystem::path base_path = opts.input_path.parent_path();
1014 if (!opts.add_paths.empty()) {
1015 std::map<std::string, patient_info> patients;
1018 std::function<void(
const directory_record&, patient_info*, study_info*, series_info*)> rebuild;
1019 rebuild = [&](
const directory_record& rec, patient_info*
patient,
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);
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);
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);
1045 }
else if (rec.type ==
"IMAGE" && series !=
nullptr) {
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");
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;
1057 series->instances.push_back(std::move(inst));
1061 for (
const auto& rec : root_records) {
1062 rebuild(rec,
nullptr,
nullptr,
nullptr);
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;
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)) {
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);
1080 for (
auto& [pid, patient] : temp_patients) {
1081 auto& p = patients[pid];
1082 if (p.patient_id.empty()) {
1083 p = std::move(patient);
1085 for (
auto& [suid, study] :
patient.studies) {
1086 if (p.studies.count(suid) == 0) {
1087 p.studies[suid] = std::move(study);
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);
1093 for (
auto& inst :
series.instances) {
1094 p.studies[suid].series[seuid].instances.push_back(std::move(inst));
1106 std::cout <<
"\nRebuilding DICOMDIR...\n";
1107 auto ds = create_dicomdir_dataset(patients, base_path, opts);
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";
1121 if (!opts.delete_paths.empty()) {
1122 std::cout <<
"Delete operation not yet implemented\n";
1126 std::cout <<
"\nDICOMDIR updated successfully.\n";
1138 ____ ____ __ __ ____ ___ ____
1139 | _ \ / ___| \/ | | _ \_ _| _ \
1140 | | | | | | |\/| | | | | | || |_) |
1141 | |_| | |___| | | | | |_| | || _ <
1142 |____/ \____|_| |_| |____/___|_| \_\
1144 DICOMDIR Creation/Management Utility
1149 if (!parse_arguments(argc, argv, opts)) {
1150 print_usage(argv[0]);
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);
1164 print_usage(argv[0]);
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.
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.
@ relative
RELATIVE - Relative dose.