39enum class output_format {
text, json };
45 std::vector<std::filesystem::path> paths;
46 output_format format{output_format::text};
47 bool recursive{
false};
50 bool show_file_info{
true};
58 std::string file_path;
59 std::uintmax_t file_size{0};
60 std::string transfer_syntax;
95 std::string photometric_interpretation;
97 size_t pixel_data_size{0};
98 bool has_pixel_data{
false};
105void print_usage(
const char* program_name) {
107DICOM Info - File Summary Utility
109Usage: )" << program_name
110 << R"( <path> [path2 ...] [options]
113 path DICOM file(s) or directory to inspect
116 -h, --help Show this help message
117 -v, --verbose Verbose output (show all available fields)
118 -q, --quiet Minimal output (file path and basic info only)
119 -f, --format <f> Output format: text (default), json
120 -r, --recursive Recursively scan directories
121 --no-file-info Don't show file information (size, transfer syntax)
127 << R"( image1.dcm image2.dcm image3.dcm
129 << R"( image.dcm --format json
131 << R"( ./dicom_folder/ --recursive
133 << R"( ./dicom_folder/ -r -q
136 Displays summary information organized by:
137 - File: Path, size, transfer syntax
138 - Patient: Name, ID, birth date, sex
139 - Study: Date, description, accession number
140 - Series: Modality, number, description
141 - Image: Dimensions, bits, photometric interpretation
145 1 Error - Invalid arguments
146 2 Error - File not found or invalid DICOM file
157bool parse_arguments(
int argc,
char* argv[], options& opts) {
162 for (
int i = 1; i < argc; ++i) {
163 std::string arg = argv[i];
165 if (arg ==
"--help" || arg ==
"-h") {
167 }
else if (arg ==
"--verbose" || arg ==
"-v") {
169 }
else if (arg ==
"--quiet" || arg ==
"-q") {
171 }
else if (arg ==
"--recursive" || arg ==
"-r") {
172 opts.recursive =
true;
173 }
else if (arg ==
"--no-file-info") {
174 opts.show_file_info =
false;
175 }
else if ((arg ==
"--format" || arg ==
"-f") && i + 1 < argc) {
176 std::string fmt = argv[++i];
178 opts.format = output_format::json;
179 }
else if (fmt ==
"text") {
180 opts.format = output_format::text;
182 std::cerr <<
"Error: Unknown format '" << fmt
183 <<
"'. Use: text, json\n";
186 }
else if (arg[0] ==
'-') {
187 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
190 opts.paths.emplace_back(arg);
194 if (opts.paths.empty()) {
195 std::cerr <<
"Error: No path specified\n";
200 opts.verbose =
false;
211std::string json_escape(
const std::string& str) {
212 std::ostringstream oss;
237 if (
static_cast<unsigned char>(c) < 0x20) {
238 oss <<
"\\u" << std::hex << std::setfill(
'0') << std::setw(4)
239 <<
static_cast<int>(c);
253std::string format_file_size(std::uintmax_t size) {
254 std::ostringstream oss;
255 if (size >= 1024 * 1024 * 1024) {
256 oss << std::fixed << std::setprecision(2)
257 <<
static_cast<double>(size) / (1024 * 1024 * 1024) <<
" GB";
258 }
else if (size >= 1024 * 1024) {
259 oss << std::fixed << std::setprecision(2)
260 <<
static_cast<double>(size) / (1024 * 1024) <<
" MB";
261 }
else if (size >= 1024) {
262 oss << std::fixed << std::setprecision(2)
263 <<
static_cast<double>(size) / 1024 <<
" KB";
265 oss << size <<
" bytes";
276bool extract_summary(
const std::filesystem::path& file_path,
277 dicom_summary& summary) {
280 auto result = dicom_file::open(file_path);
281 if (result.is_err()) {
285 auto& file = result.value();
286 const auto& dataset = file.dataset();
289 summary.file_path = file_path.string();
290 summary.file_size = std::filesystem::file_size(file_path);
291 summary.transfer_syntax = file.transfer_syntax().name();
292 summary.transfer_syntax_uid = file.transfer_syntax().uid();
293 summary.sop_class_uid = file.sop_class_uid();
294 summary.sop_instance_uid = file.sop_instance_uid();
297 summary.patient_name = dataset.get_string(tags::patient_name);
298 summary.patient_id = dataset.get_string(tags::patient_id);
299 summary.patient_birth_date = dataset.get_string(tags::patient_birth_date);
300 summary.patient_sex = dataset.get_string(tags::patient_sex);
303 summary.study_date = dataset.get_string(tags::study_date);
304 summary.study_time = dataset.get_string(tags::study_time);
305 summary.study_description = dataset.get_string(tags::study_description);
306 summary.study_instance_uid = dataset.get_string(tags::study_instance_uid);
307 summary.accession_number = dataset.get_string(tags::accession_number);
310 summary.modality = dataset.get_string(tags::modality);
311 summary.series_number = dataset.get_string(tags::series_number);
312 summary.series_description = dataset.get_string(tags::series_description);
313 summary.series_instance_uid = dataset.get_string(tags::series_instance_uid);
316 summary.instance_number = dataset.get_string(tags::instance_number);
317 summary.acquisition_date = dataset.get_string(dicom_tag{0x0008, 0x0022});
318 summary.acquisition_time = dataset.get_string(dicom_tag{0x0008, 0x0032});
321 if (
auto val = dataset.get_numeric<uint16_t>(tags::rows)) {
324 if (
auto val = dataset.get_numeric<uint16_t>(tags::columns)) {
327 if (
auto val = dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0100})) {
330 if (
auto val = dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0101})) {
333 if (
auto val = dataset.get_numeric<uint16_t>(dicom_tag{0x0028, 0x0002})) {
334 summary.samples_per_pixel = *val;
336 summary.photometric_interpretation =
337 dataset.get_string(dicom_tag{0x0028, 0x0004});
338 summary.number_of_frames = dataset.get_string(dicom_tag{0x0028, 0x0008});
341 auto*
pixel_data = dataset.get(dicom_tag{0x7FE0, 0x0010});
342 if (pixel_data !=
nullptr) {
355void print_summary_text(
const dicom_summary& summary,
const options& opts) {
358 std::cout <<
summary.file_path;
359 if (!
summary.modality.empty()) {
360 std::cout <<
" [" <<
summary.modality <<
"]";
369 const int label_width = 24;
371 std::cout <<
"========================================\n";
374 if (opts.show_file_info) {
375 std::cout <<
"File Information\n";
376 std::cout <<
"----------------------------------------\n";
377 std::cout << std::left << std::setw(label_width) <<
" Path:"
379 std::cout << std::left << std::setw(label_width) <<
" Size:"
380 << format_file_size(
summary.file_size) <<
" ("
381 <<
summary.file_size <<
" bytes)\n";
382 std::cout << std::left << std::setw(label_width) <<
" Transfer Syntax:"
383 <<
summary.transfer_syntax <<
"\n";
385 std::cout << std::left << std::setw(label_width) <<
" TS UID:"
386 <<
summary.transfer_syntax_uid <<
"\n";
387 std::cout << std::left << std::setw(label_width) <<
" SOP Class:"
388 <<
summary.sop_class_uid <<
"\n";
389 std::cout << std::left << std::setw(label_width) <<
" SOP Instance:"
390 <<
summary.sop_instance_uid <<
"\n";
396 std::cout <<
"Patient Information\n";
397 std::cout <<
"----------------------------------------\n";
398 std::cout << std::left << std::setw(label_width) <<
" Name:"
399 << (
summary.patient_name.empty() ?
"(not specified)"
402 std::cout << std::left << std::setw(label_width) <<
" ID:"
403 << (
summary.patient_id.empty() ?
"(not specified)"
406 if (opts.verbose || !
summary.patient_birth_date.empty()) {
407 std::cout << std::left << std::setw(label_width) <<
" Birth Date:"
408 << (
summary.patient_birth_date.empty() ?
"(not specified)"
412 if (opts.verbose || !
summary.patient_sex.empty()) {
413 std::cout << std::left << std::setw(label_width) <<
" Sex:"
414 << (
summary.patient_sex.empty() ?
"(not specified)"
421 std::cout <<
"Study Information\n";
422 std::cout <<
"----------------------------------------\n";
423 std::cout << std::left << std::setw(label_width) <<
" Date:"
424 << (
summary.study_date.empty() ?
"(not specified)"
427 if (opts.verbose || !
summary.study_time.empty()) {
428 std::cout << std::left << std::setw(label_width) <<
" Time:"
429 << (
summary.study_time.empty() ?
"(not specified)"
433 if (opts.verbose || !
summary.study_description.empty()) {
434 std::cout << std::left << std::setw(label_width) <<
" Description:"
435 << (
summary.study_description.empty() ?
"(not specified)"
439 if (opts.verbose || !
summary.accession_number.empty()) {
440 std::cout << std::left << std::setw(label_width) <<
" Accession #:"
441 << (
summary.accession_number.empty() ?
"(not specified)"
446 std::cout << std::left << std::setw(label_width) <<
" Study UID:"
447 <<
summary.study_instance_uid <<
"\n";
452 std::cout <<
"Series Information\n";
453 std::cout <<
"----------------------------------------\n";
454 std::cout << std::left << std::setw(label_width) <<
" Modality:"
455 << (
summary.modality.empty() ?
"(not specified)"
458 if (opts.verbose || !
summary.series_number.empty()) {
459 std::cout << std::left << std::setw(label_width) <<
" Series #:"
460 << (
summary.series_number.empty() ?
"(not specified)"
464 if (opts.verbose || !
summary.series_description.empty()) {
465 std::cout << std::left << std::setw(label_width) <<
" Description:"
466 << (
summary.series_description.empty() ?
"(not specified)"
471 std::cout << std::left << std::setw(label_width) <<
" Series UID:"
472 <<
summary.series_instance_uid <<
"\n";
478 std::cout <<
"Instance Information\n";
479 std::cout <<
"----------------------------------------\n";
480 std::cout << std::left << std::setw(label_width) <<
" Instance #:"
481 << (
summary.instance_number.empty() ?
"(not specified)"
484 if (!
summary.acquisition_date.empty()) {
485 std::cout << std::left << std::setw(label_width) <<
" Acquisition Date:"
486 <<
summary.acquisition_date <<
"\n";
488 if (!
summary.acquisition_time.empty()) {
489 std::cout << std::left << std::setw(label_width) <<
" Acquisition Time:"
490 <<
summary.acquisition_time <<
"\n";
497 std::cout <<
"Image Information\n";
498 std::cout <<
"----------------------------------------\n";
500 std::cout << std::left << std::setw(label_width) <<
" Dimensions:"
503 if (
summary.bits_allocated > 0) {
504 std::cout << std::left << std::setw(label_width) <<
" Bits:"
505 <<
summary.bits_stored <<
" stored / "
506 <<
summary.bits_allocated <<
" allocated\n";
508 if (
summary.samples_per_pixel > 0) {
509 std::cout << std::left << std::setw(label_width) <<
" Samples/Pixel:"
510 <<
summary.samples_per_pixel <<
"\n";
512 if (!
summary.photometric_interpretation.empty()) {
513 std::cout << std::left << std::setw(label_width) <<
" Photometric:"
514 <<
summary.photometric_interpretation <<
"\n";
516 if (!
summary.number_of_frames.empty()) {
517 std::cout << std::left << std::setw(label_width) <<
" Frames:"
518 <<
summary.number_of_frames <<
"\n";
521 std::cout << std::left << std::setw(label_width) <<
" Pixel Data:"
522 << format_file_size(
summary.pixel_data_size) <<
"\n";
527 std::cout <<
"========================================\n";
536void print_summary_json(
const dicom_summary& summary,
const options& opts,
537 bool is_last =
true) {
541 if (opts.show_file_info) {
542 std::cout <<
" \"file\": {\n";
543 std::cout <<
" \"path\": \"" << json_escape(
summary.file_path) <<
"\",\n";
544 std::cout <<
" \"size\": " <<
summary.file_size <<
",\n";
545 std::cout <<
" \"sizeFormatted\": \"" << format_file_size(
summary.file_size)
547 std::cout <<
" \"transferSyntax\": \"" << json_escape(
summary.transfer_syntax)
549 std::cout <<
" \"transferSyntaxUID\": \""
550 << json_escape(
summary.transfer_syntax_uid) <<
"\",\n";
551 std::cout <<
" \"sopClassUID\": \"" << json_escape(
summary.sop_class_uid)
553 std::cout <<
" \"sopInstanceUID\": \"" << json_escape(
summary.sop_instance_uid)
555 std::cout <<
" },\n";
559 std::cout <<
" \"patient\": {\n";
560 std::cout <<
" \"name\": \"" << json_escape(
summary.patient_name) <<
"\",\n";
561 std::cout <<
" \"id\": \"" << json_escape(
summary.patient_id) <<
"\",\n";
562 std::cout <<
" \"birthDate\": \"" << json_escape(
summary.patient_birth_date)
564 std::cout <<
" \"sex\": \"" << json_escape(
summary.patient_sex) <<
"\"\n";
565 std::cout <<
" },\n";
568 std::cout <<
" \"study\": {\n";
569 std::cout <<
" \"date\": \"" << json_escape(
summary.study_date) <<
"\",\n";
570 std::cout <<
" \"time\": \"" << json_escape(
summary.study_time) <<
"\",\n";
571 std::cout <<
" \"description\": \"" << json_escape(
summary.study_description)
573 std::cout <<
" \"instanceUID\": \"" << json_escape(
summary.study_instance_uid)
575 std::cout <<
" \"accessionNumber\": \"" << json_escape(
summary.accession_number)
577 std::cout <<
" },\n";
580 std::cout <<
" \"series\": {\n";
581 std::cout <<
" \"modality\": \"" << json_escape(
summary.modality) <<
"\",\n";
582 std::cout <<
" \"number\": \"" << json_escape(
summary.series_number) <<
"\",\n";
583 std::cout <<
" \"description\": \"" << json_escape(
summary.series_description)
585 std::cout <<
" \"instanceUID\": \"" << json_escape(
summary.series_instance_uid)
587 std::cout <<
" },\n";
590 std::cout <<
" \"instance\": {\n";
591 std::cout <<
" \"number\": \"" << json_escape(
summary.instance_number) <<
"\",\n";
592 std::cout <<
" \"acquisitionDate\": \"" << json_escape(
summary.acquisition_date)
594 std::cout <<
" \"acquisitionTime\": \"" << json_escape(
summary.acquisition_time)
596 std::cout <<
" },\n";
599 std::cout <<
" \"image\": {\n";
600 std::cout <<
" \"rows\": " <<
summary.rows <<
",\n";
601 std::cout <<
" \"columns\": " <<
summary.columns <<
",\n";
602 std::cout <<
" \"bitsAllocated\": " <<
summary.bits_allocated <<
",\n";
603 std::cout <<
" \"bitsStored\": " <<
summary.bits_stored <<
",\n";
604 std::cout <<
" \"samplesPerPixel\": " <<
summary.samples_per_pixel <<
",\n";
605 std::cout <<
" \"photometricInterpretation\": \""
606 << json_escape(
summary.photometric_interpretation) <<
"\",\n";
607 std::cout <<
" \"numberOfFrames\": \"" << json_escape(
summary.number_of_frames)
609 std::cout <<
" \"hasPixelData\": " << (
summary.has_pixel_data ?
"true" :
"false")
611 std::cout <<
" \"pixelDataSize\": " <<
summary.pixel_data_size <<
"\n";
614 std::cout <<
"}" << (is_last ?
"" :
",") <<
"\n";
624int process_file(
const std::filesystem::path& file_path,
const options& opts,
625 bool is_last =
true) {
628 if (!extract_summary(file_path, summary)) {
630 std::cerr <<
"Error: Failed to read DICOM file: " << file_path.string()
636 if (opts.format == output_format::json) {
637 print_summary_json(summary, opts, is_last);
639 print_summary_text(summary, opts);
651std::vector<std::filesystem::path> collect_files(
652 const std::filesystem::path& dir_path,
bool recursive) {
653 std::vector<std::filesystem::path> files;
655 auto is_dicom_file = [](
const std::filesystem::path& p) {
656 auto ext = p.extension().string();
657 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
658 return ext ==
".dcm" || ext ==
".dicom" || ext.empty();
662 for (
const auto& entry :
663 std::filesystem::recursive_directory_iterator(dir_path)) {
664 if (entry.is_regular_file() && is_dicom_file(entry.path())) {
665 files.push_back(entry.path());
669 for (
const auto& entry : std::filesystem::directory_iterator(dir_path)) {
670 if (entry.is_regular_file() && is_dicom_file(entry.path())) {
671 files.push_back(entry.path());
676 std::sort(files.begin(), files.end());
682int main(
int argc,
char* argv[]) {
685 if (!parse_arguments(argc, argv, opts)) {
687 ____ ____ __ __ ___ _ _ _____ ___
688 | _ \ / ___| \/ | |_ _| \ | | ___/ _ \
689 | | | | | | |\/| | | || \| | |_ | | | |
690 | |_| | |___| | | | | || |\ | _|| |_| |
691 |____/ \____|_| |_| |___|_| \_|_| \___/
693 DICOM File Summary Utility
695 print_usage(argv[0]);
700 std::vector<std::filesystem::path> all_files;
701 for (
const auto& path : opts.paths) {
702 if (!std::filesystem::exists(path)) {
703 std::cerr <<
"Error: Path does not exist: " << path.string() <<
"\n";
707 if (std::filesystem::is_directory(path)) {
708 auto dir_files = collect_files(path, opts.recursive);
709 all_files.insert(all_files.end(), dir_files.begin(), dir_files.end());
711 all_files.push_back(path);
715 if (all_files.empty()) {
716 std::cerr <<
"Error: No DICOM files found\n";
721 if (opts.format == output_format::text && !opts.quiet) {
723 ____ ____ __ __ ___ _ _ _____ ___
724 | _ \ / ___| \/ | |_ _| \ | | ___/ _ \
725 | | | | | | |\/| | | || \| | |_ | | | |
726 | |_| | |___| | | | | || |\ | _|| |_| |
727 |____/ \____|_| |_| |___|_| \_|_| \___/
729 DICOM File Summary Utility
732 if (all_files.size() > 1) {
733 std::cout <<
"Processing " << all_files.size() <<
" files...\n\n";
738 if (opts.format == output_format::json && all_files.size() > 1) {
743 for (
size_t i = 0; i < all_files.size(); ++i) {
744 bool is_last = (i == all_files.size() - 1);
745 if (process_file(all_files[i], opts, is_last) != 0) {
750 if (opts.format == output_format::text && !is_last && !opts.quiet) {
756 if (opts.format == output_format::json && all_files.size() > 1) {
DICOM Part 10 file handling for reading/writing DICOM files.
Compile-time constants for commonly used DICOM tags.
@ summary
Statistical summary (min, max, mean, percentiles)