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 <set>
35#include <sstream>
36#include <string>
37#include <vector>
38
39namespace {
40
44enum class output_format { human_readable, json, xml };
45
49struct options {
50 std::filesystem::path path;
51 std::vector<std::string> filter_tags; // Keywords to filter
52 std::string search_keyword; // Search by tag name
53 bool pixel_info{false};
54 output_format format{output_format::human_readable};
55 bool recursive{false};
56 bool summary{false};
57 bool show_meta{true};
58 bool verbose{false};
59 bool quiet{false};
60 int max_depth{-1}; // -1 means unlimited
61 bool no_pixel{false}; // Exclude pixel data
62 bool show_private{false};
63 std::string charset{"UTF-8"};
64};
65
69struct scan_summary {
70 size_t total_files{0};
71 size_t valid_files{0};
72 size_t invalid_files{0};
73 std::map<std::string, size_t> modalities;
74 std::map<std::string, size_t> sop_classes;
75};
76
81void print_usage(const char* program_name) {
82 std::cout << R"(
83DICOM Dump - File Inspection Utility
84
85Usage: )" << program_name
86 << R"( <path> [options]
87
88Arguments:
89 path DICOM file or directory to inspect
90
91Options:
92 -h, --help Show this help message
93 -v, --verbose Verbose output mode
94 -q, --quiet Minimal output mode (errors only)
95 -f, --format <format> Output format: text (default), json, xml
96 -t, --tag <tag> Output specific tag only (e.g., 0010,0010)
97 --tags <list> Show only specific tags (comma-separated keywords)
98 Example: --tags PatientName,PatientID,StudyDate
99 -s, --search <keyword> Search by tag name (case-insensitive)
100 -d, --depth <n> Limit sequence output depth (default: unlimited)
101 --pixel-info Show pixel data information
102 --no-pixel Exclude pixel data from output
103 --show-private Show private tags (hidden by default)
104 --charset <charset> Specify character set (default: UTF-8)
105 --recursive, -r Recursively scan directories
106 --summary Show summary only (for directories)
107 --no-meta Don't show File Meta Information
108
109Examples:
110 )" << program_name
111 << R"( image.dcm
112 )" << program_name
113 << R"( image.dcm --tags PatientName,PatientID,StudyDate
114 )" << program_name
115 << R"( image.dcm -t 0010,0010
116 )" << program_name
117 << R"( image.dcm --search Patient
118 )" << program_name
119 << R"( image.dcm --pixel-info
120 )" << program_name
121 << R"( image.dcm --format json
122 )" << program_name
123 << R"( image.dcm --format xml
124 )" << program_name
125 << R"( image.dcm -d 2 # Limit sequence depth to 2
126 )" << program_name
127 << R"( ./dicom_folder/ --recursive --summary
128
129Output Format:
130 Human-readable (text) output shows tags in the format:
131 (GGGG,EEEE) VR Keyword [value]
132
133 JSON output provides DICOM PS3.18-compatible structured data.
134 XML output provides DICOM Native XML format (PS3.19).
135
136Exit Codes:
137 0 Success - File(s) parsed successfully
138 1 Error - Invalid arguments
139 2 Error - File not found or invalid DICOM file
140)";
141}
142
155std::string parse_tag_string(const std::string& tag_str) {
156 std::string s = tag_str;
157 // Remove parentheses if present
158 if (!s.empty() && s.front() == '(') s.erase(0, 1);
159 if (!s.empty() && s.back() == ')') s.pop_back();
160 // Validate format: should be GGGG,EEEE or GGGGEEEE
161 if (s.length() == 9 && s[4] == ',') {
162 return s; // Already in GGGG,EEEE format
163 } else if (s.length() == 8) {
164 return s.substr(0, 4) + "," + s.substr(4, 4);
165 }
166 return "";
167}
168
169bool parse_arguments(int argc, char* argv[], options& opts) {
170 if (argc < 2) {
171 return false;
172 }
173
174 for (int i = 1; i < argc; ++i) {
175 std::string arg = argv[i];
176
177 if (arg == "--help" || arg == "-h") {
178 return false;
179 } else if (arg == "--tags" && i + 1 < argc) {
180 // Parse comma-separated tag keywords
181 std::string tags_str = argv[++i];
182 std::stringstream ss(tags_str);
183 std::string tag;
184 while (std::getline(ss, tag, ',')) {
185 // Trim whitespace
186 tag.erase(0, tag.find_first_not_of(" \t"));
187 tag.erase(tag.find_last_not_of(" \t") + 1);
188 if (!tag.empty()) {
189 opts.filter_tags.push_back(tag);
190 }
191 }
192 } else if ((arg == "--tag" || arg == "-t") && i + 1 < argc) {
193 // Parse single tag in format (GGGG,EEEE) or GGGG,EEEE
194 std::string tag_str = parse_tag_string(argv[++i]);
195 if (tag_str.empty()) {
196 std::cerr << "Error: Invalid tag format. Use GGGG,EEEE (e.g., 0010,0010)\n";
197 return false;
198 }
199 opts.filter_tags.push_back(tag_str);
200 } else if ((arg == "--search" || arg == "-s") && i + 1 < argc) {
201 opts.search_keyword = argv[++i];
202 } else if ((arg == "--depth" || arg == "-d") && i + 1 < argc) {
203 try {
204 opts.max_depth = std::stoi(argv[++i]);
205 if (opts.max_depth < 0) {
206 std::cerr << "Error: Depth must be non-negative\n";
207 return false;
208 }
209 } catch (...) {
210 std::cerr << "Error: Invalid depth value\n";
211 return false;
212 }
213 } else if (arg == "--pixel-info") {
214 opts.pixel_info = true;
215 } else if (arg == "--no-pixel") {
216 opts.no_pixel = true;
217 } else if (arg == "--show-private") {
218 opts.show_private = true;
219 } else if (arg == "--charset" && i + 1 < argc) {
220 opts.charset = argv[++i];
221 } else if ((arg == "--format" || arg == "-f") && i + 1 < argc) {
222 std::string fmt = argv[++i];
223 if (fmt == "json") {
224 opts.format = output_format::json;
225 } else if (fmt == "human" || fmt == "text") {
226 opts.format = output_format::human_readable;
227 } else if (fmt == "xml") {
228 opts.format = output_format::xml;
229 } else {
230 std::cerr << "Error: Unknown format '" << fmt << "'. Use: text, json, xml\n";
231 return false;
232 }
233 } else if (arg == "--recursive" || arg == "-r") {
234 opts.recursive = true;
235 } else if (arg == "--summary") {
236 opts.summary = true;
237 } else if (arg == "--no-meta") {
238 opts.show_meta = false;
239 } else if (arg == "--verbose" || arg == "-v") {
240 opts.verbose = true;
241 } else if (arg == "--quiet" || arg == "-q") {
242 opts.quiet = true;
243 } else if (arg[0] == '-') {
244 std::cerr << "Error: Unknown option '" << arg << "'\n";
245 return false;
246 } else if (opts.path.empty()) {
247 opts.path = arg;
248 } else {
249 std::cerr << "Error: Multiple paths specified\n";
250 return false;
251 }
252 }
253
254 if (opts.path.empty()) {
255 std::cerr << "Error: No path specified\n";
256 return false;
257 }
258
259 // Quiet mode overrides verbose
260 if (opts.quiet) {
261 opts.verbose = false;
262 }
263
264 return true;
265}
266
272std::string json_escape(const std::string& str) {
273 std::ostringstream oss;
274 for (char c : str) {
275 switch (c) {
276 case '"':
277 oss << "\\\"";
278 break;
279 case '\\':
280 oss << "\\\\";
281 break;
282 case '\b':
283 oss << "\\b";
284 break;
285 case '\f':
286 oss << "\\f";
287 break;
288 case '\n':
289 oss << "\\n";
290 break;
291 case '\r':
292 oss << "\\r";
293 break;
294 case '\t':
295 oss << "\\t";
296 break;
297 default:
298 if (static_cast<unsigned char>(c) < 0x20) {
299 oss << "\\u" << std::hex << std::setfill('0') << std::setw(4)
300 << static_cast<int>(c);
301 } else {
302 oss << c;
303 }
304 }
305 }
306 return oss.str();
307}
308
315std::string format_hex(std::span<const uint8_t> data, size_t max_bytes = 32) {
316 std::ostringstream oss;
317 size_t count = std::min(data.size(), max_bytes);
318
319 for (size_t i = 0; i < count; ++i) {
320 if (i > 0) {
321 oss << "\\";
322 }
323 oss << std::hex << std::setfill('0') << std::setw(2)
324 << static_cast<int>(data[i]);
325 }
326
327 if (data.size() > max_bytes) {
328 oss << "...";
329 }
330
331 return oss.str();
332}
333
340std::string format_value(const kcenon::pacs::core::dicom_element& element,
341 size_t max_length = 64) {
342 using namespace kcenon::pacs::encoding;
343
344 auto vr = element.vr();
345
346 // Handle empty values
347 if (element.is_empty()) {
348 return "(empty)";
349 }
350
351 // Handle sequences
352 if (element.is_sequence()) {
353 const auto& items = element.sequence_items();
354 return "SQ (" + std::to_string(items.size()) + " items)";
355 }
356
357 // Handle binary VRs
358 if (is_binary_vr(vr)) {
359 auto data = element.raw_data();
360 return "[" + format_hex(data) + "] (" + std::to_string(data.size()) +
361 " bytes)";
362 }
363
364 // Handle string VRs
365 if (is_string_vr(vr)) {
366 auto result = element.as_string();
367 if (result.is_ok()) {
368 std::string value = result.value();
369 if (value.length() > max_length) {
370 value = value.substr(0, max_length - 3) + "...";
371 }
372 return value;
373 }
374 // Fall through to raw data display on error
375 }
376
377 // Handle numeric VRs
378 if (is_numeric_vr(vr)) {
379 switch (vr) {
380 case vr_type::US:
381 if (auto val = element.as_numeric<uint16_t>(); val.is_ok())
382 return std::to_string(val.value());
383 break;
384 case vr_type::SS:
385 if (auto val = element.as_numeric<int16_t>(); val.is_ok())
386 return std::to_string(val.value());
387 break;
388 case vr_type::UL:
389 if (auto val = element.as_numeric<uint32_t>(); val.is_ok())
390 return std::to_string(val.value());
391 break;
392 case vr_type::SL:
393 if (auto val = element.as_numeric<int32_t>(); val.is_ok())
394 return std::to_string(val.value());
395 break;
396 case vr_type::FL:
397 if (auto val = element.as_numeric<float>(); val.is_ok())
398 return std::to_string(val.value());
399 break;
400 case vr_type::FD:
401 if (auto val = element.as_numeric<double>(); val.is_ok())
402 return std::to_string(val.value());
403 break;
404 case vr_type::UV:
405 if (auto val = element.as_numeric<uint64_t>(); val.is_ok())
406 return std::to_string(val.value());
407 break;
408 case vr_type::SV:
409 if (auto val = element.as_numeric<int64_t>(); val.is_ok())
410 return std::to_string(val.value());
411 break;
412 default:
413 break;
414 }
415 // Fall through to raw data display on error
416 }
417
418 // Fallback: show raw bytes
419 auto data = element.raw_data();
420 return "[" + format_hex(data) + "]";
421}
422
429bool contains_ci(const std::string& haystack, const std::string& needle) {
430 if (needle.empty()) return true;
431 if (haystack.empty()) return false;
432
433 std::string h = haystack;
434 std::string n = needle;
435 std::transform(h.begin(), h.end(), h.begin(), ::tolower);
436 std::transform(n.begin(), n.end(), n.begin(), ::tolower);
437 return h.find(n) != std::string::npos;
438}
439
445bool is_private_tag(const kcenon::pacs::core::dicom_tag& tag) {
446 return (tag.group() % 2) != 0;
447}
448
454bool is_pixel_data_tag(const kcenon::pacs::core::dicom_tag& tag) {
455 return tag.group() == 0x7FE0 && tag.element() == 0x0010;
456}
457
465bool should_display_tag(const kcenon::pacs::core::dicom_tag& tag,
466 const options& opts,
468 // Handle pixel data exclusion
469 if (opts.no_pixel && is_pixel_data_tag(tag)) {
470 return false;
471 }
472
473 // Handle private tags
474 if (is_private_tag(tag) && !opts.show_private) {
475 return false;
476 }
477
478 auto info = dict.find(tag);
479 std::string keyword = info ? std::string(info->keyword) : "";
480
481 // Handle search keyword
482 if (!opts.search_keyword.empty()) {
483 if (!contains_ci(keyword, opts.search_keyword) &&
484 !contains_ci(tag.to_string(), opts.search_keyword)) {
485 return false;
486 }
487 }
488
489 // Handle tag filter list
490 if (!opts.filter_tags.empty()) {
491 for (const auto& filter : opts.filter_tags) {
492 // Check if filter is a tag string (contains comma or is 8 hex chars)
493 if (filter.find(',') != std::string::npos || filter.length() == 8) {
494 // Compare as tag string
495 std::string tag_str = tag.to_string();
496 // Remove parentheses for comparison
497 std::string tag_cmp = tag_str.substr(1, tag_str.length() - 2);
498 if (tag_cmp == filter) {
499 return true;
500 }
501 } else {
502 // Compare as keyword
503 if (keyword == filter) {
504 return true;
505 }
506 }
507 }
508 return false;
509 }
510
511 return true;
512}
513
521void print_dataset_human(const kcenon::pacs::core::dicom_dataset& dataset,
522 const options& opts, int current_depth = 0, int indent = 0) {
523 using namespace kcenon::pacs::core;
524 using namespace kcenon::pacs::encoding;
525
526 auto& dict = dicom_dictionary::instance();
527 std::string indent_str(indent * 2, ' ');
528
529 for (const auto& [tag, element] : dataset) {
530 // Check filter
531 if (!should_display_tag(tag, opts, dict)) {
532 continue;
533 }
534
535 // Get tag info from dictionary
536 auto info = dict.find(tag);
537 std::string keyword =
538 info ? std::string(info->keyword) : "UnknownTag";
539
540 // Mark private tags
541 if (is_private_tag(tag)) {
542 keyword = "Private: " + keyword;
543 }
544
545 // Format: (GGGG,EEEE) VR Keyword [value]
546 std::cout << indent_str << tag.to_string() << " "
547 << to_string(element.vr()) << " " << std::left
548 << std::setw(36 - indent * 2) << keyword;
549
550 // Handle sequences specially
551 if (element.is_sequence()) {
552 const auto& items = element.sequence_items();
553 std::cout << "(" << items.size() << " items)\n";
554
555 // Check depth limit
556 if (opts.max_depth >= 0 && current_depth >= opts.max_depth) {
557 std::cout << indent_str << " ... (depth limit reached)\n";
558 continue;
559 }
560
561 int item_num = 0;
562 for (const auto& item : items) {
563 std::cout << indent_str << " > Item #" << item_num++ << "\n";
564 print_dataset_human(item, opts, current_depth + 1, indent + 2);
565 }
566 } else {
567 std::cout << "[" << format_value(element) << "]\n";
568 }
569 }
570}
571
579void print_dataset_json(const kcenon::pacs::core::dicom_dataset& dataset,
580 const options& opts, int current_depth = 0, int indent = 2) {
581 using namespace kcenon::pacs::core;
582 using namespace kcenon::pacs::encoding;
583
584 auto& dict = dicom_dictionary::instance();
585 std::string indent_str(indent, ' ');
586
587 std::cout << "{\n";
588
589 bool first = true;
590
591 for (const auto& [tag, element] : dataset) {
592 // Check filter
593 if (!should_display_tag(tag, opts, dict)) {
594 continue;
595 }
596
597 if (!first) {
598 std::cout << ",\n";
599 }
600 first = false;
601
602 auto info = dict.find(tag);
603 std::string keyword =
604 info ? std::string(info->keyword) : tag.to_string();
605
606 // Use DICOM PS3.18 JSON format: tag as key in GGGGEEEE format
607 std::string tag_key = tag.to_string();
608 tag_key.erase(std::remove(tag_key.begin(), tag_key.end(), '('), tag_key.end());
609 tag_key.erase(std::remove(tag_key.begin(), tag_key.end(), ')'), tag_key.end());
610 tag_key.erase(std::remove(tag_key.begin(), tag_key.end(), ','), tag_key.end());
611
612 std::cout << indent_str << " \"" << tag_key << "\": {\n";
613 std::cout << indent_str << " \"vr\": \"" << to_string(element.vr()) << "\"";
614
615 if (element.is_sequence()) {
616 std::cout << ",\n" << indent_str << " \"Value\": [\n";
617
618 // Check depth limit
619 if (opts.max_depth >= 0 && current_depth >= opts.max_depth) {
620 std::cout << indent_str << " { \"_note\": \"depth limit reached\" }\n";
621 } else {
622 const auto& items = element.sequence_items();
623 for (size_t i = 0; i < items.size(); ++i) {
624 std::cout << indent_str << " ";
625 print_dataset_json(items[i], opts, current_depth + 1, indent + 6);
626 if (i < items.size() - 1) {
627 std::cout << ",";
628 }
629 std::cout << "\n";
630 }
631 }
632 std::cout << indent_str << " ]\n";
633 } else {
634 std::string value = json_escape(format_value(element, 256));
635 std::cout << ",\n" << indent_str << " \"Value\": [\"" << value << "\"]\n";
636 }
637
638 std::cout << indent_str << " }";
639 }
640
641 std::cout << "\n" << indent_str << "}";
642}
643
649std::string xml_escape(const std::string& str) {
650 std::ostringstream oss;
651 for (char c : str) {
652 switch (c) {
653 case '<': oss << "&lt;"; break;
654 case '>': oss << "&gt;"; break;
655 case '&': oss << "&amp;"; break;
656 case '"': oss << "&quot;"; break;
657 case '\'': oss << "&apos;"; break;
658 default:
659 if (static_cast<unsigned char>(c) < 0x20 && c != '\t' && c != '\n' && c != '\r') {
660 oss << "&#" << static_cast<int>(static_cast<unsigned char>(c)) << ";";
661 } else {
662 oss << c;
663 }
664 }
665 }
666 return oss.str();
667}
668
676void print_dataset_xml(const kcenon::pacs::core::dicom_dataset& dataset,
677 const options& opts, int current_depth = 0, int indent = 2) {
678 using namespace kcenon::pacs::core;
679 using namespace kcenon::pacs::encoding;
680
681 auto& dict = dicom_dictionary::instance();
682 std::string indent_str(indent, ' ');
683
684 for (const auto& [tag, element] : dataset) {
685 // Check filter
686 if (!should_display_tag(tag, opts, dict)) {
687 continue;
688 }
689
690 auto info = dict.find(tag);
691 std::string keyword = info ? std::string(info->keyword) : "UnknownTag";
692 std::string vr_str{to_string(element.vr())};
693
694 // Format tag as GGGGEEEE
695 std::string tag_str = tag.to_string();
696 tag_str.erase(std::remove(tag_str.begin(), tag_str.end(), '('), tag_str.end());
697 tag_str.erase(std::remove(tag_str.begin(), tag_str.end(), ')'), tag_str.end());
698 tag_str.erase(std::remove(tag_str.begin(), tag_str.end(), ','), tag_str.end());
699
700 std::cout << indent_str << "<DicomAttribute tag=\"" << tag_str
701 << "\" vr=\"" << vr_str
702 << "\" keyword=\"" << xml_escape(keyword) << "\"";
703
704 if (element.is_sequence()) {
705 std::cout << ">\n";
706
707 // Check depth limit
708 if (opts.max_depth >= 0 && current_depth >= opts.max_depth) {
709 std::cout << indent_str << " <!-- depth limit reached -->\n";
710 } else {
711 const auto& items = element.sequence_items();
712 int item_num = 1;
713 for (const auto& item : items) {
714 std::cout << indent_str << " <Item number=\"" << item_num++ << "\">\n";
715 print_dataset_xml(item, opts, current_depth + 1, indent + 4);
716 std::cout << indent_str << " </Item>\n";
717 }
718 }
719 std::cout << indent_str << "</DicomAttribute>\n";
720 } else if (element.is_empty()) {
721 std::cout << "/>\n";
722 } else {
723 std::cout << ">\n";
724
725 // Handle PersonName VR specially
726 if (element.vr() == vr_type::PN) {
727 auto result = element.as_string();
728 if (result.is_ok()) {
729 std::cout << indent_str << " <PersonName>\n";
730 std::cout << indent_str << " <Alphabetic>\n";
731 std::cout << indent_str << " <FamilyName>" << xml_escape(result.value()) << "</FamilyName>\n";
732 std::cout << indent_str << " </Alphabetic>\n";
733 std::cout << indent_str << " </PersonName>\n";
734 }
735 } else {
736 std::string value = format_value(element, 1024);
737 std::cout << indent_str << " <Value number=\"1\">" << xml_escape(value) << "</Value>\n";
738 }
739
740 std::cout << indent_str << "</DicomAttribute>\n";
741 }
742 }
743}
744
749void print_pixel_info(const kcenon::pacs::core::dicom_dataset& dataset) {
750 using namespace kcenon::pacs::core;
751 using namespace kcenon::pacs::encoding;
752
753 std::cout << "\n# Pixel Data Information\n";
754 std::cout << "----------------------------------------\n";
755
756 // Rows and Columns
757 auto rows = dataset.get_numeric<uint16_t>(tags::rows);
758 auto cols = dataset.get_numeric<uint16_t>(tags::columns);
759 if (rows && cols) {
760 std::cout << " Dimensions: " << *cols << " x " << *rows << "\n";
761 }
762
763 // Bits Allocated / Stored / High Bit
764 auto bits_allocated =
765 dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0100});
766 auto bits_stored = dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0101});
767 auto high_bit = dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0102});
768 if (bits_allocated) {
769 std::cout << " Bits Allocated: " << *bits_allocated << "\n";
770 }
771 if (bits_stored) {
772 std::cout << " Bits Stored: " << *bits_stored << "\n";
773 }
774 if (high_bit) {
775 std::cout << " High Bit: " << *high_bit << "\n";
776 }
777
778 // Pixel Representation
779 auto pixel_rep = dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0103});
780 if (pixel_rep) {
781 std::cout << " Pixel Rep: "
782 << (*pixel_rep == 0 ? "Unsigned" : "Signed") << "\n";
783 }
784
785 // Samples Per Pixel
786 auto samples = dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0002});
787 if (samples) {
788 std::cout << " Samples/Pixel: " << *samples << "\n";
789 }
790
791 // Photometric Interpretation
792 auto photometric = dataset.get_string(dicom_tag{0x0028, 0x0004});
793 if (!photometric.empty()) {
794 std::cout << " Photometric: " << photometric << "\n";
795 }
796
797 // Number of Frames
798 auto frames = dataset.get_string(dicom_tag{0x0028, 0x0008});
799 if (!frames.empty()) {
800 std::cout << " Number of Frames: " << frames << "\n";
801 }
802
803 // Pixel Data element info
804 auto* pixel_data = dataset.get(dicom_tag{0x7FE0, 0x0010});
805 if (pixel_data != nullptr) {
806 std::cout << " Pixel Data: " << pixel_data->length()
807 << " bytes\n";
808 std::cout << " Pixel Data VR: " << to_string(pixel_data->vr())
809 << "\n";
810 } else {
811 std::cout << " Pixel Data: (not present)\n";
812 }
813
814 std::cout << "----------------------------------------\n";
815}
816
823int dump_file(const std::filesystem::path& file_path, const options& opts) {
824 using namespace kcenon::pacs::core;
825
826 auto result = dicom_file::open(file_path);
827 if (result.is_err()) {
828 std::cerr << "Error: Failed to open '" << file_path.string()
829 << "': " << result.error().message << "\n";
830 return 2;
831 }
832
833 // Quiet mode: only show errors
834 if (opts.quiet) {
835 return 0;
836 }
837
838 auto& file = result.value();
839
840 if (opts.format == output_format::json) {
841 // JSON output (DICOM PS3.18 compatible)
842 std::cout << "{\n";
843 std::cout << " \"file\": \"" << json_escape(file_path.string())
844 << "\",\n";
845 std::cout << " \"transferSyntax\": \""
846 << file.transfer_syntax().name() << "\",\n";
847 std::cout << " \"sopClassUID\": \"" << file.sop_class_uid() << "\",\n";
848 std::cout << " \"sopInstanceUID\": \"" << file.sop_instance_uid()
849 << "\",\n";
850
851 if (opts.show_meta) {
852 std::cout << " \"metaInformation\": ";
853 print_dataset_json(file.meta_information(), opts);
854 std::cout << ",\n";
855 }
856
857 std::cout << " \"dataset\": ";
858 print_dataset_json(file.dataset(), opts);
859 std::cout << "\n}\n";
860 } else if (opts.format == output_format::xml) {
861 // XML output (DICOM Native XML PS3.19)
862 std::cout << "<?xml version=\"1.0\" encoding=\"" << opts.charset << "\"?>\n";
863 std::cout << "<NativeDicomModel>\n";
864 std::cout << " <!-- File: " << xml_escape(file_path.string()) << " -->\n";
865 std::cout << " <!-- Transfer Syntax: " << file.transfer_syntax().name() << " -->\n";
866 std::cout << " <!-- SOP Class: " << file.sop_class_uid() << " -->\n";
867 std::cout << " <!-- SOP Instance: " << file.sop_instance_uid() << " -->\n";
868
869 if (opts.show_meta) {
870 std::cout << " <!-- File Meta Information -->\n";
871 print_dataset_xml(file.meta_information(), opts);
872 }
873
874 std::cout << " <!-- Dataset -->\n";
875 print_dataset_xml(file.dataset(), opts);
876 std::cout << "</NativeDicomModel>\n";
877 } else {
878 // Human-readable (text) output
879 std::cout << "# File: " << file_path.string() << "\n";
880 std::cout << "# Transfer Syntax: " << file.transfer_syntax().name()
881 << " (" << file.transfer_syntax().uid() << ")\n";
882 std::cout << "# SOP Class: " << file.sop_class_uid() << "\n";
883 std::cout << "# SOP Instance: " << file.sop_instance_uid() << "\n";
884 std::cout << "\n";
885
886 if (opts.show_meta) {
887 std::cout << "# File Meta Information\n";
888 print_dataset_human(file.meta_information(), opts);
889 std::cout << "\n";
890 }
891
892 std::cout << "# Dataset\n";
893 print_dataset_human(file.dataset(), opts);
894
895 if (opts.pixel_info) {
896 print_pixel_info(file.dataset());
897 }
898 }
899
900 return 0;
901}
902
909void scan_directory(const std::filesystem::path& dir_path, const options& opts,
910 scan_summary& summary) {
911 using namespace kcenon::pacs::core;
912
913 auto process_file = [&](const std::filesystem::path& file_path) {
914 ++summary.total_files;
915
916 auto result = dicom_file::open(file_path);
917 if (result.is_err()) {
918 ++summary.invalid_files;
919 if (opts.verbose) {
920 std::cerr << " Invalid: " << file_path.filename().string()
921 << "\n";
922 }
923 return;
924 }
925
926 ++summary.valid_files;
927 auto& file = result.value();
928
929 // Collect modality
930 auto modality = file.dataset().get_string(dicom_tag{0x0008, 0x0060});
931 if (!modality.empty()) {
932 summary.modalities[modality]++;
933 }
934
935 // Collect SOP Class
936 auto sop_class = file.sop_class_uid();
937 if (!sop_class.empty()) {
938 summary.sop_classes[sop_class]++;
939 }
940
941 if (opts.verbose) {
942 std::cout << " OK: " << file_path.filename().string();
943 if (!modality.empty()) {
944 std::cout << " [" << modality << "]";
945 }
946 std::cout << "\n";
947 }
948 };
949
950 if (opts.recursive) {
951 for (const auto& entry :
952 std::filesystem::recursive_directory_iterator(dir_path)) {
953 if (entry.is_regular_file()) {
954 auto ext = entry.path().extension().string();
955 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
956 if (ext == ".dcm" || ext == ".dicom" || ext.empty()) {
957 process_file(entry.path());
958 }
959 }
960 }
961 } else {
962 for (const auto& entry : std::filesystem::directory_iterator(dir_path)) {
963 if (entry.is_regular_file()) {
964 auto ext = entry.path().extension().string();
965 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
966 if (ext == ".dcm" || ext == ".dicom" || ext.empty()) {
967 process_file(entry.path());
968 }
969 }
970 }
971 }
972}
973
979void print_summary(const scan_summary& summary, const options& opts) {
980 if (opts.format == output_format::json) {
981 std::cout << "{\n";
982 std::cout << " \"totalFiles\": " << summary.total_files << ",\n";
983 std::cout << " \"validFiles\": " << summary.valid_files << ",\n";
984 std::cout << " \"invalidFiles\": " << summary.invalid_files << ",\n";
985
986 std::cout << " \"modalities\": {\n";
987 size_t mod_count = 0;
988 for (const auto& [modality, count] : summary.modalities) {
989 std::cout << " \"" << modality << "\": " << count;
990 if (++mod_count < summary.modalities.size()) {
991 std::cout << ",";
992 }
993 std::cout << "\n";
994 }
995 std::cout << " },\n";
996
997 std::cout << " \"sopClasses\": {\n";
998 size_t sop_count = 0;
999 for (const auto& [sop_class, count] : summary.sop_classes) {
1000 std::cout << " \"" << sop_class << "\": " << count;
1001 if (++sop_count < summary.sop_classes.size()) {
1002 std::cout << ",";
1003 }
1004 std::cout << "\n";
1005 }
1006 std::cout << " }\n";
1007 std::cout << "}\n";
1008 } else {
1009 std::cout << "\n";
1010 std::cout << "========================================\n";
1011 std::cout << " Directory Summary\n";
1012 std::cout << "========================================\n";
1013 std::cout << " Total files: " << summary.total_files << "\n";
1014 std::cout << " Valid DICOM: " << summary.valid_files << "\n";
1015 std::cout << " Invalid/Other: " << summary.invalid_files << "\n";
1016 std::cout << "\n";
1017
1018 if (!summary.modalities.empty()) {
1019 std::cout << " Modalities:\n";
1020 for (const auto& [modality, count] : summary.modalities) {
1021 std::cout << " " << std::left << std::setw(10) << modality
1022 << " " << count << " file(s)\n";
1023 }
1024 std::cout << "\n";
1025 }
1026
1027 if (!summary.sop_classes.empty() && opts.verbose) {
1028 std::cout << " SOP Classes:\n";
1029 for (const auto& [sop_class, count] : summary.sop_classes) {
1030 std::cout << " " << sop_class << ": " << count
1031 << " file(s)\n";
1032 }
1033 }
1034
1035 std::cout << "========================================\n";
1036 }
1037}
1038
1039} // namespace
1040
1041int main(int argc, char* argv[]) {
1042 options opts;
1043
1044 if (!parse_arguments(argc, argv, opts)) {
1045 // Show banner for help
1046 std::cout << R"(
1047 ____ ____ __ __ ____ _ _ __ __ ____
1048 | _ \ / ___| \/ | | _ \| | | | \/ | _ \
1049 | | | | | | |\/| | | | | | | | | |\/| | |_) |
1050 | |_| | |___| | | | | |_| | |_| | | | | __/
1051 |____/ \____|_| |_| |____/ \___/|_| |_|_|
1052
1053 DICOM File Inspection Utility
1054)" << "\n";
1055 print_usage(argv[0]);
1056 return 1;
1057 }
1058
1059 // Check if path exists
1060 if (!std::filesystem::exists(opts.path)) {
1061 std::cerr << "Error: Path does not exist: " << opts.path.string()
1062 << "\n";
1063 return 2;
1064 }
1065
1066 // Show banner only in non-quiet text/human mode
1067 if (!opts.quiet && opts.format == output_format::human_readable) {
1068 std::cout << R"(
1069 ____ ____ __ __ ____ _ _ __ __ ____
1070 | _ \ / ___| \/ | | _ \| | | | \/ | _ \
1071 | | | | | | |\/| | | | | | | | | |\/| | |_) |
1072 | |_| | |___| | | | | |_| | |_| | | | | __/
1073 |____/ \____|_| |_| |____/ \___/|_| |_|_|
1074
1075 DICOM File Inspection Utility
1076)" << "\n";
1077 }
1078
1079 // Handle directory vs file
1080 if (std::filesystem::is_directory(opts.path)) {
1081 if (opts.summary) {
1082 if (!opts.quiet) {
1083 std::cout << "Scanning directory: " << opts.path.string() << "\n";
1084 if (opts.recursive) {
1085 std::cout << "Mode: Recursive\n";
1086 }
1087 std::cout << "\n";
1088 }
1089
1090 scan_summary summary;
1091 scan_directory(opts.path, opts, summary);
1092 if (!opts.quiet) {
1093 print_summary(summary, opts);
1094 }
1095 return summary.invalid_files > 0 ? 1 : 0;
1096 } else {
1097 // Dump each file
1098 int exit_code = 0;
1099 auto process = [&](const std::filesystem::path& file_path) {
1100 auto ext = file_path.extension().string();
1101 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
1102 if (ext == ".dcm" || ext == ".dicom" || ext.empty()) {
1103 if (dump_file(file_path, opts) != 0) {
1104 exit_code = 1;
1105 }
1106 std::cout << "\n";
1107 }
1108 };
1109
1110 if (opts.recursive) {
1111 for (const auto& entry :
1112 std::filesystem::recursive_directory_iterator(opts.path)) {
1113 if (entry.is_regular_file()) {
1114 process(entry.path());
1115 }
1116 }
1117 } else {
1118 for (const auto& entry :
1119 std::filesystem::directory_iterator(opts.path)) {
1120 if (entry.is_regular_file()) {
1121 process(entry.path());
1122 }
1123 }
1124 }
1125
1126 return exit_code;
1127 }
1128 } else {
1129 // Single file
1130 return dump_file(opts.path, opts);
1131 }
1132}
if(!color.empty()) style.color
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
auto get_numeric(dicom_tag tag) const -> std::optional< T >
Get the numeric value of an element.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
auto find(dicom_tag tag) const -> std::optional< tag_info >
Find tag metadata by DICOM tag.
auto is_sequence() const noexcept -> bool
Check if this element is a sequence.
auto as_numeric() const -> kcenon::pacs::Result< T >
Get the value as a numeric type.
auto raw_data() const noexcept -> std::span< const uint8_t >
Get the raw data bytes.
constexpr auto vr() const noexcept -> encoding::vr_type
Get the element's VR.
auto as_string() const -> kcenon::pacs::Result< std::string >
Get the value as a string.
auto sequence_items() -> std::vector< dicom_dataset > &
Get mutable access to sequence items.
auto is_empty() const noexcept -> bool
Check if the element has no value.
auto to_string() const -> std::string
Convert to string representation.
constexpr auto group() const noexcept -> uint16_t
Get the group number.
Definition dicom_tag.h:90
constexpr auto element() const noexcept -> uint16_t
Get the element number.
Definition dicom_tag.h:98
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 high_bit
High Bit.
constexpr dicom_tag bits_allocated
Bits Allocated.
constexpr dicom_tag rows
Rows.
constexpr dicom_tag bits_stored
Bits Stored.
constexpr dicom_tag pixel_data
Pixel Data.
constexpr dicom_tag modality
Modality.
constexpr bool is_numeric_vr(vr_type vr) noexcept
Checks if a VR is a numeric type.
Definition vr_type.h:214
constexpr bool is_binary_vr(vr_type vr) noexcept
Checks if a VR is a binary/raw byte type.
Definition vr_type.h:196
constexpr bool is_string_vr(vr_type vr) noexcept
Checks if a VR is a string type.
Definition vr_type.h:175
@ summary
Statistical summary (min, max, mean, percentiles)
vr_encoding vr