PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
23#include "anonymizer.h"
24
30
31#include <algorithm>
32#include <atomic>
33#include <cctype>
34#include <chrono>
35#include <filesystem>
36#include <fstream>
37#include <iomanip>
38#include <iostream>
39#include <sstream>
40#include <string>
41#include <thread>
42#include <vector>
43
44namespace {
45
49enum class operation_type {
50 insert, // Add or modify tag (tag doesn't need to exist)
51 modify, // Modify tag (tag must exist)
52 erase, // Delete single tag
53 erase_all // Delete all matching tags including in sequences
54};
55
59struct modification {
60 operation_type op;
62 std::string value; // For insert/modify operations
63 std::string keyword; // Original keyword or tag string for error messages
64};
65
69struct options {
70 std::vector<std::filesystem::path> input_paths;
71 std::filesystem::path output_path;
72 std::vector<modification> modifications;
73 std::filesystem::path script_file;
74 bool erase_private{false};
75 bool gen_study_uid{false};
76 bool gen_series_uid{false};
77 bool gen_instance_uid{false};
78 bool create_backup{true};
79 bool in_place{false};
80 bool recursive{false};
81 bool verbose{false};
82 bool dry_run{false};
83};
84
88class uid_generator {
89public:
90 std::string generate() {
91 static std::atomic<uint64_t> counter{0};
92 auto now = std::chrono::system_clock::now();
93 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
94 now.time_since_epoch())
95 .count();
96 return std::string(uid_root_) + "." + std::to_string(timestamp) + "." +
97 std::to_string(++counter);
98 }
99
100private:
101 static constexpr const char* uid_root_ = "1.2.826.0.1.3680043.8.1055.2";
102};
103
108void print_usage(const char* program_name) {
109 std::cout << "\nDICOM Modify - Tag Modification Utility\n\n";
110 std::cout << "Usage: " << program_name << " [options] <dicom-file>...\n\n";
111 std::cout << "Arguments:\n";
112 std::cout << " dicom-file One or more DICOM files to modify\n\n";
113 std::cout << "Tag Modification Options:\n";
114 std::cout << " -i, --insert <tag=value> Add or modify tag (creates if not exists)\n";
115 std::cout << " Example: -i \"(0010,0010)=Anonymous\"\n";
116 std::cout << " Example: -i PatientName=Anonymous\n";
117 std::cout << " -m, --modify <tag=value> Modify existing tag (error if not exists)\n";
118 std::cout << " Example: -m \"(0010,0020)=NEW_ID\"\n";
119 std::cout << " -e, --erase <tag> Delete tag\n";
120 std::cout << " Example: -e \"(0010,1000)\"\n";
121 std::cout << " Example: -e OtherPatientIDs\n";
122 std::cout << " -ea, --erase-all <tag> Delete all matching tags (including in sequences)\n";
123 std::cout << " -ep, --erase-private Delete all private tags\n\n";
124 std::cout << "UID Generation Options:\n";
125 std::cout << " -gst, --gen-stud-uid Generate new StudyInstanceUID\n";
126 std::cout << " -gse, --gen-ser-uid Generate new SeriesInstanceUID\n";
127 std::cout << " -gin, --gen-inst-uid Generate new SOPInstanceUID\n\n";
128 std::cout << "Output Options:\n";
129 std::cout << " -o, --output <path> Output file or directory\n";
130 std::cout << " -nb, --no-backup Do not create backup file (.bak)\n\n";
131 std::cout << "Script Option:\n";
132 std::cout << " --script <file> Read modification commands from script file\n\n";
133 std::cout << "Processing Options:\n";
134 std::cout << " -r, --recursive Process directories recursively\n";
135 std::cout << " --dry-run Show what would be done without modifying\n";
136 std::cout << " -v, --verbose Show detailed output\n";
137 std::cout << " -h, --help Show this help message\n\n";
138 std::cout << "Tag Format:\n";
139 std::cout << " Tags can be specified in two formats:\n";
140 std::cout << " - Numeric: (GGGG,EEEE) e.g., (0010,0010)\n";
141 std::cout << " - Keyword: e.g., PatientName, PatientID\n\n";
142 std::cout << "Script File Format:\n";
143 std::cout << " Lines starting with # are comments\n";
144 std::cout << " i (0010,0010)=Anonymous Insert/modify tag\n";
145 std::cout << " m (0008,0050)=ACC001 Modify existing tag\n";
146 std::cout << " e (0010,1000) Erase tag\n";
147 std::cout << " ea (0010,1001) Erase all matching tags\n\n";
148 std::cout << "Examples:\n";
149 std::cout << " " << program_name << " -i \"(0010,0010)=Anonymous\" patient.dcm\n";
150 std::cout << " " << program_name << " -m PatientName=\"Hong^Gildong\" -o modified.dcm patient.dcm\n";
151 std::cout << " " << program_name << " -gst -gse -gin -o anonymized.dcm patient.dcm\n";
152 std::cout << " " << program_name << " --script modify.txt *.dcm\n";
153 std::cout << " " << program_name << " -i PatientID=NEW_ID patient.dcm (in-place with backup)\n";
154 std::cout << " " << program_name << " -i PatientID=NEW_ID -nb patient.dcm (no backup)\n\n";
155 std::cout << "Exit Codes:\n";
156 std::cout << " 0 Success\n";
157 std::cout << " 1 Invalid arguments\n";
158 std::cout << " 2 File/processing error\n";
159}
160
166std::optional<kcenon::pacs::core::dicom_tag> parse_tag_string(
167 const std::string& tag_str) {
168 std::string s = tag_str;
169
170 // Remove parentheses if present
171 if (!s.empty() && s.front() == '(') {
172 s.erase(0, 1);
173 }
174 if (!s.empty() && s.back() == ')') {
175 s.pop_back();
176 }
177
178 // Remove spaces
179 s.erase(std::remove(s.begin(), s.end(), ' '), s.end());
180
181 // Parse GGGG,EEEE format
182 size_t comma_pos = s.find(',');
183 if (comma_pos != std::string::npos) {
184 try {
185 uint16_t group =
186 static_cast<uint16_t>(std::stoul(s.substr(0, comma_pos), nullptr, 16));
187 uint16_t element =
188 static_cast<uint16_t>(std::stoul(s.substr(comma_pos + 1), nullptr, 16));
189 return kcenon::pacs::core::dicom_tag{group, element};
190 } catch (...) {
191 return std::nullopt;
192 }
193 }
194
195 // Parse GGGGEEEE format (8 hex chars)
196 if (s.length() == 8) {
197 try {
198 uint16_t group =
199 static_cast<uint16_t>(std::stoul(s.substr(0, 4), nullptr, 16));
200 uint16_t element =
201 static_cast<uint16_t>(std::stoul(s.substr(4, 4), nullptr, 16));
202 return kcenon::pacs::core::dicom_tag{group, element};
203 } catch (...) {
204 return std::nullopt;
205 }
206 }
207
208 return std::nullopt;
209}
210
216std::optional<kcenon::pacs::core::dicom_tag> resolve_tag(const std::string& str) {
217 // First, try as numeric tag format
218 if (str.find('(') != std::string::npos || str.find(',') != std::string::npos ||
219 (str.length() == 8 && std::all_of(str.begin(), str.end(), ::isxdigit))) {
220 return parse_tag_string(str);
221 }
222
223 // Try as keyword
225 auto info = dict.find_by_keyword(str);
226 if (info) {
227 return info->tag;
228 }
229
230 return std::nullopt;
231}
232
240bool parse_modification_string(const std::string& str, operation_type op,
241 modification& mod) {
242 mod.op = op;
243
244 if (op == operation_type::erase || op == operation_type::erase_all) {
245 // Just tag, no value
246 auto tag_opt = resolve_tag(str);
247 if (!tag_opt) {
248 return false;
249 }
250 mod.tag = *tag_opt;
251 mod.keyword = str;
252 return true;
253 }
254
255 // For insert/modify: tag=value
256 auto eq_pos = str.find('=');
257 if (eq_pos == std::string::npos || eq_pos == 0) {
258 return false;
259 }
260
261 std::string tag_str = str.substr(0, eq_pos);
262 std::string value = str.substr(eq_pos + 1);
263
264 // Remove surrounding quotes from value
265 if (value.size() >= 2) {
266 if ((value.front() == '"' && value.back() == '"') ||
267 (value.front() == '\'' && value.back() == '\'')) {
268 value = value.substr(1, value.size() - 2);
269 }
270 }
271
272 auto tag_opt = resolve_tag(tag_str);
273 if (!tag_opt) {
274 return false;
275 }
276
277 mod.tag = *tag_opt;
278 mod.value = value;
279 mod.keyword = tag_str;
280 return true;
281}
282
289bool parse_script_file(const std::filesystem::path& script_path,
290 std::vector<modification>& modifications) {
291 std::ifstream file(script_path);
292 if (!file.is_open()) {
293 std::cerr << "Error: Cannot open script file: " << script_path.string()
294 << "\n";
295 return false;
296 }
297
298 std::string line;
299 int line_num = 0;
300 while (std::getline(file, line)) {
301 ++line_num;
302
303 // Trim whitespace
304 size_t start = line.find_first_not_of(" \t");
305 if (start == std::string::npos) {
306 continue; // Empty line
307 }
308 line = line.substr(start);
309
310 // Skip comments
311 if (line[0] == '#') {
312 continue;
313 }
314
315 // Remove trailing comment
316 auto comment_pos = line.find('#');
317 if (comment_pos != std::string::npos) {
318 line = line.substr(0, comment_pos);
319 // Trim trailing whitespace
320 size_t end = line.find_last_not_of(" \t");
321 if (end != std::string::npos) {
322 line = line.substr(0, end + 1);
323 }
324 }
325
326 if (line.empty()) {
327 continue;
328 }
329
330 // Parse command
331 operation_type op;
332 std::string arg;
333
334 if (line.length() >= 2 && line[0] == 'i' && line[1] == ' ') {
335 op = operation_type::insert;
336 arg = line.substr(2);
337 } else if (line.length() >= 2 && line[0] == 'm' && line[1] == ' ') {
338 op = operation_type::modify;
339 arg = line.substr(2);
340 } else if (line.length() >= 2 && line[0] == 'e' && line[1] == ' ') {
341 op = operation_type::erase;
342 arg = line.substr(2);
343 } else if (line.length() >= 3 && line.substr(0, 2) == "ea" &&
344 line[2] == ' ') {
345 op = operation_type::erase_all;
346 arg = line.substr(3);
347 } else {
348 std::cerr << "Warning: Invalid command in script file at line "
349 << line_num << ": " << line << "\n";
350 continue;
351 }
352
353 // Trim argument
354 start = arg.find_first_not_of(" \t");
355 if (start != std::string::npos) {
356 arg = arg.substr(start);
357 }
358
359 modification mod;
360 if (!parse_modification_string(arg, op, mod)) {
361 std::cerr << "Warning: Invalid modification in script file at line "
362 << line_num << ": " << arg << "\n";
363 continue;
364 }
365
366 modifications.push_back(std::move(mod));
367 }
368
369 return true;
370}
371
379bool parse_arguments(int argc, char* argv[], options& opts) {
380 if (argc < 2) {
381 return false;
382 }
383
384 for (int i = 1; i < argc; ++i) {
385 std::string arg = argv[i];
386
387 if (arg == "--help" || arg == "-h") {
388 return false;
389 } else if ((arg == "-o" || arg == "--output") && i + 1 < argc) {
390 opts.output_path = argv[++i];
391 } else if ((arg == "-i" || arg == "--insert") && i + 1 < argc) {
392 modification mod;
393 if (!parse_modification_string(argv[++i], operation_type::insert,
394 mod)) {
395 std::cerr << "Error: Invalid --insert format. Use tag=value "
396 "(e.g., \"(0010,0010)=Anonymous\")\n";
397 return false;
398 }
399 opts.modifications.push_back(std::move(mod));
400 } else if ((arg == "-m" || arg == "--modify") && i + 1 < argc) {
401 modification mod;
402 if (!parse_modification_string(argv[++i], operation_type::modify,
403 mod)) {
404 std::cerr << "Error: Invalid --modify format. Use tag=value "
405 "(e.g., \"(0010,0020)=NEW_ID\")\n";
406 return false;
407 }
408 opts.modifications.push_back(std::move(mod));
409 } else if ((arg == "-e" || arg == "--erase") && i + 1 < argc) {
410 modification mod;
411 if (!parse_modification_string(argv[++i], operation_type::erase,
412 mod)) {
413 std::cerr << "Error: Invalid --erase format. Use tag "
414 "(e.g., \"(0010,1000)\")\n";
415 return false;
416 }
417 opts.modifications.push_back(std::move(mod));
418 } else if ((arg == "-ea" || arg == "--erase-all") && i + 1 < argc) {
419 modification mod;
420 if (!parse_modification_string(argv[++i], operation_type::erase_all,
421 mod)) {
422 std::cerr << "Error: Invalid --erase-all format. Use tag "
423 "(e.g., \"(0010,1001)\")\n";
424 return false;
425 }
426 opts.modifications.push_back(std::move(mod));
427 } else if (arg == "-ep" || arg == "--erase-private") {
428 opts.erase_private = true;
429 } else if (arg == "-gst" || arg == "--gen-stud-uid") {
430 opts.gen_study_uid = true;
431 } else if (arg == "-gse" || arg == "--gen-ser-uid") {
432 opts.gen_series_uid = true;
433 } else if (arg == "-gin" || arg == "--gen-inst-uid") {
434 opts.gen_instance_uid = true;
435 } else if (arg == "-nb" || arg == "--no-backup") {
436 opts.create_backup = false;
437 } else if (arg == "--script" && i + 1 < argc) {
438 opts.script_file = argv[++i];
439 } else if (arg == "-r" || arg == "--recursive") {
440 opts.recursive = true;
441 } else if (arg == "--dry-run") {
442 opts.dry_run = true;
443 } else if (arg == "-v" || arg == "--verbose") {
444 opts.verbose = true;
445 } else if (arg[0] == '-') {
446 std::cerr << "Error: Unknown option '" << arg << "'\n";
447 return false;
448 } else {
449 opts.input_paths.emplace_back(arg);
450 }
451 }
452
453 // Parse script file if provided
454 if (!opts.script_file.empty()) {
455 if (!parse_script_file(opts.script_file, opts.modifications)) {
456 return false;
457 }
458 }
459
460 // Validation
461 if (opts.input_paths.empty()) {
462 std::cerr << "Error: No input files specified\n";
463 return false;
464 }
465
466 // Must have at least one modification operation
467 if (opts.modifications.empty() && !opts.erase_private &&
468 !opts.gen_study_uid && !opts.gen_series_uid && !opts.gen_instance_uid) {
469 std::cerr << "Error: No modification operation specified\n";
470 return false;
471 }
472
473 // Determine if in-place mode
474 if (opts.output_path.empty()) {
475 opts.in_place = true;
476 }
477
478 return true;
479}
480
485void remove_private_tags_recursive(kcenon::pacs::core::dicom_dataset& dataset) {
486 std::vector<kcenon::pacs::core::dicom_tag> private_tags;
487
488 for (const auto& [tag, element] : dataset) {
489 if (tag.is_private()) {
490 private_tags.push_back(tag);
491 }
492 }
493
494 for (const auto& tag : private_tags) {
495 dataset.remove(tag);
496 }
497
498 // Process sequences
499 for (auto& [tag, element] : dataset) {
500 if (element.is_sequence()) {
501 for (auto& item : element.sequence_items()) {
502 remove_private_tags_recursive(item);
503 }
504 }
505 }
506}
507
514size_t remove_tag_recursive(kcenon::pacs::core::dicom_dataset& dataset,
516 size_t count = 0;
517
518 if (dataset.contains(tag)) {
519 dataset.remove(tag);
520 ++count;
521 }
522
523 // Process sequences
524 for (auto& [seq_tag, element] : dataset) {
525 if (element.is_sequence()) {
526 for (auto& item : element.sequence_items()) {
527 count += remove_tag_recursive(item, tag);
528 }
529 }
530 }
531
532 return count;
533}
534
542bool apply_modifications(kcenon::pacs::core::dicom_dataset& dataset, const options& opts,
543 uid_generator& uid_gen) {
544 using namespace kcenon::pacs::core;
545 using namespace kcenon::pacs::encoding;
546
547 auto& dict = dicom_dictionary::instance();
548
549 // Apply tag modifications
550 for (const auto& mod : opts.modifications) {
551 switch (mod.op) {
552 case operation_type::insert: {
553 auto info = dict.find(mod.tag);
554 vr_type vr = info ? static_cast<vr_type>(info->vr) : vr_type::LO;
555
556 if (opts.verbose) {
557 std::cout << " Insert " << mod.tag.to_string() << " ("
558 << mod.keyword << ") = \"" << mod.value << "\"\n";
559 }
560
561 dataset.set_string(mod.tag, vr, mod.value);
562 break;
563 }
564
565 case operation_type::modify: {
566 if (!dataset.contains(mod.tag)) {
567 std::cerr << " Error: Tag " << mod.tag.to_string() << " ("
568 << mod.keyword << ") does not exist (use -i to insert)\n";
569 return false;
570 }
571
572 auto info = dict.find(mod.tag);
573 vr_type vr = info ? static_cast<vr_type>(info->vr) : vr_type::LO;
574
575 if (opts.verbose) {
576 std::cout << " Modify " << mod.tag.to_string() << " ("
577 << mod.keyword << ") = \"" << mod.value << "\"\n";
578 }
579
580 dataset.set_string(mod.tag, vr, mod.value);
581 break;
582 }
583
584 case operation_type::erase: {
585 if (opts.verbose) {
586 std::cout << " Erase " << mod.tag.to_string() << " ("
587 << mod.keyword << ")\n";
588 }
589
590 dataset.remove(mod.tag);
591 break;
592 }
593
594 case operation_type::erase_all: {
595 size_t count = remove_tag_recursive(dataset, mod.tag);
596 if (opts.verbose) {
597 std::cout << " Erase all " << mod.tag.to_string() << " ("
598 << mod.keyword << ") - removed " << count
599 << " instance(s)\n";
600 }
601 break;
602 }
603 }
604 }
605
606 // Erase private tags
607 if (opts.erase_private) {
608 if (opts.verbose) {
609 std::cout << " Erasing all private tags...\n";
610 }
611 remove_private_tags_recursive(dataset);
612 }
613
614 // Generate new UIDs
615 if (opts.gen_study_uid) {
616 std::string new_uid = uid_gen.generate();
617 if (opts.verbose) {
618 std::cout << " Generate new StudyInstanceUID: " << new_uid << "\n";
619 }
620 dataset.set_string(tags::study_instance_uid, vr_type::UI, new_uid);
621 }
622
623 if (opts.gen_series_uid) {
624 std::string new_uid = uid_gen.generate();
625 if (opts.verbose) {
626 std::cout << " Generate new SeriesInstanceUID: " << new_uid << "\n";
627 }
628 dataset.set_string(tags::series_instance_uid, vr_type::UI, new_uid);
629 }
630
631 if (opts.gen_instance_uid) {
632 std::string new_uid = uid_gen.generate();
633 if (opts.verbose) {
634 std::cout << " Generate new SOPInstanceUID: " << new_uid << "\n";
635 }
636 dataset.set_string(tags::sop_instance_uid, vr_type::UI, new_uid);
637 }
638
639 return true;
640}
641
645struct process_stats {
646 size_t total_files{0};
647 size_t successful{0};
648 size_t failed{0};
649};
650
656bool create_backup(const std::filesystem::path& file_path) {
657 auto backup_path = file_path;
658 backup_path += ".bak";
659
660 std::error_code ec;
661 std::filesystem::copy_file(file_path, backup_path,
662 std::filesystem::copy_options::overwrite_existing,
663 ec);
664 if (ec) {
665 std::cerr << "Warning: Failed to create backup file: " << backup_path.string()
666 << " (" << ec.message() << ")\n";
667 return false;
668 }
669
670 return true;
671}
672
681bool process_file(const std::filesystem::path& input_path,
682 const std::filesystem::path& output_path, const options& opts,
683 uid_generator& uid_gen) {
684 using namespace kcenon::pacs::core;
685
686 if (opts.verbose) {
687 std::cout << "Processing: " << input_path.string() << "\n";
688 }
689
690 // Dry run mode
691 if (opts.dry_run) {
692 std::cout << "Would modify: " << input_path.string() << "\n";
693 for (const auto& mod : opts.modifications) {
694 switch (mod.op) {
695 case operation_type::insert:
696 std::cout << " Insert " << mod.tag.to_string() << " = \""
697 << mod.value << "\"\n";
698 break;
699 case operation_type::modify:
700 std::cout << " Modify " << mod.tag.to_string() << " = \""
701 << mod.value << "\"\n";
702 break;
703 case operation_type::erase:
704 std::cout << " Erase " << mod.tag.to_string() << "\n";
705 break;
706 case operation_type::erase_all:
707 std::cout << " Erase all " << mod.tag.to_string() << "\n";
708 break;
709 }
710 }
711 if (opts.erase_private) {
712 std::cout << " Erase all private tags\n";
713 }
714 if (opts.gen_study_uid) {
715 std::cout << " Generate new StudyInstanceUID\n";
716 }
717 if (opts.gen_series_uid) {
718 std::cout << " Generate new SeriesInstanceUID\n";
719 }
720 if (opts.gen_instance_uid) {
721 std::cout << " Generate new SOPInstanceUID\n";
722 }
723 std::cout << " Output: " << output_path.string() << "\n";
724 return true;
725 }
726
727 // Create backup for in-place modification
728 if (opts.in_place && opts.create_backup) {
729 if (!create_backup(input_path)) {
730 // Continue anyway, just warn
731 }
732 }
733
734 // Open input file
735 auto result = dicom_file::open(input_path);
736 if (result.is_err()) {
737 std::cerr << "Error: Failed to open '" << input_path.string()
738 << "': " << result.error().message << "\n";
739 return false;
740 }
741
742 auto file = std::move(result.value());
743 auto& dataset = file.dataset();
744
745 // Apply modifications
746 if (!apply_modifications(dataset, opts, uid_gen)) {
747 return false;
748 }
749
750 // Create output file with same transfer syntax
751 auto output_file = dicom_file::create(std::move(dataset), file.transfer_syntax());
752
753 // Ensure output directory exists
754 auto output_dir = output_path.parent_path();
755 if (!output_dir.empty() && !std::filesystem::exists(output_dir)) {
756 std::filesystem::create_directories(output_dir);
757 }
758
759 // Save
760 auto save_result = output_file.save(output_path);
761 if (save_result.is_err()) {
762 std::cerr << "Error: Failed to save '" << output_path.string()
763 << "': " << save_result.error().message << "\n";
764 return false;
765 }
766
767 if (opts.verbose) {
768 std::cout << " Saved: " << output_path.string() << "\n";
769 }
770
771 return true;
772}
773
779void process_inputs(const options& opts, process_stats& stats) {
780 uid_generator uid_gen;
781
782 for (const auto& input_path : opts.input_paths) {
783 if (!std::filesystem::exists(input_path)) {
784 std::cerr << "Error: Path does not exist: " << input_path.string()
785 << "\n";
786 ++stats.failed;
787 continue;
788 }
789
790 if (std::filesystem::is_directory(input_path)) {
791 // Directory mode
792 auto process_entry = [&](const std::filesystem::path& file_path) {
793 auto ext = file_path.extension().string();
794 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
795 if (ext != ".dcm" && ext != ".dicom" && !ext.empty()) {
796 return; // Skip non-DICOM files
797 }
798
799 ++stats.total_files;
800
801 std::filesystem::path output_path;
802 if (opts.in_place) {
803 output_path = file_path;
804 } else {
805 auto relative =
806 std::filesystem::relative(file_path, input_path);
807 output_path = opts.output_path / relative;
808 }
809
810 if (process_file(file_path, output_path, opts, uid_gen)) {
811 ++stats.successful;
812 } else {
813 ++stats.failed;
814 }
815 };
816
817 if (opts.recursive) {
818 for (const auto& entry :
819 std::filesystem::recursive_directory_iterator(input_path)) {
820 if (entry.is_regular_file()) {
821 process_entry(entry.path());
822 }
823 }
824 } else {
825 for (const auto& entry :
826 std::filesystem::directory_iterator(input_path)) {
827 if (entry.is_regular_file()) {
828 process_entry(entry.path());
829 }
830 }
831 }
832 } else {
833 // Single file mode
834 ++stats.total_files;
835
836 std::filesystem::path output_path;
837 if (opts.in_place) {
838 output_path = input_path;
839 } else {
840 output_path = opts.output_path;
841 }
842
843 if (process_file(input_path, output_path, opts, uid_gen)) {
844 ++stats.successful;
845 } else {
846 ++stats.failed;
847 }
848 }
849 }
850}
851
856void print_summary(const process_stats& stats) {
857 if (stats.total_files > 1) {
858 std::cout << "\n";
859 std::cout << "========================================\n";
860 std::cout << " Processing Summary\n";
861 std::cout << "========================================\n";
862 std::cout << " Total files: " << stats.total_files << "\n";
863 std::cout << " Successful: " << stats.successful << "\n";
864 std::cout << " Failed: " << stats.failed << "\n";
865 std::cout << "========================================\n";
866 }
867}
868
869} // namespace
870
871int main(int argc, char* argv[]) {
872 std::cout << R"(
873 ____ ____ __ __ __ __ ___ ____ ___ _______ __
874 | _ \ / ___| \/ | | \/ |/ _ \| _ \_ _| ___\ \ / /
875 | | | | | | |\/| | | |\/| | | | | | | | || |_ \ V /
876 | |_| | |___| | | | | | | | |_| | |_| | || _| | |
877 |____/ \____|_| |_| |_| |_|\___/|____/___|_| |_|
878
879 DICOM Tag Modification Utility
880)" << "\n";
881
882 options opts;
883
884 if (!parse_arguments(argc, argv, opts)) {
885 print_usage(argv[0]);
886 return 1;
887 }
888
889 process_stats stats;
890 process_inputs(opts, stats);
891
892 print_summary(stats);
893
894 if (stats.failed > 0) {
895 return 2;
896 }
897
898 if (stats.total_files == 1 && stats.successful == 1) {
899 std::cout << "Successfully modified file.\n";
900 }
901
902 return 0;
903}
auto remove(dicom_tag tag) -> bool
Remove an element from the dataset.
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto contains(dicom_tag tag) const noexcept -> bool
Check if the dataset contains an element with the given tag.
static auto instance() -> dicom_dictionary &
Get the singleton instance.
auto to_string() const -> std::string
Convert to string representation.
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.
DICOM Anonymization - Patient data removal/replacement.
int main()
Definition main.cpp:84
@ modify
Clinician modifies AI result (e.g., edits segmentation)
@ LO
Long String (64 chars max)
@ counter
Monotonic increasing value.
@ op
Ophthalmic Photography / Tomography.
vr_encoding vr