PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
26
27#include <algorithm>
28#include <chrono>
29#include <filesystem>
30#include <iomanip>
31#include <iostream>
32#include <string>
33#include <vector>
34
35namespace {
36
40struct options {
41 std::filesystem::path input_path;
42 std::filesystem::path output_path;
43 std::string target_transfer_syntax;
44 int quality{90};
45 bool recursive{false};
46 bool verify{false};
47 bool overwrite{false};
48 bool verbose{false};
49 bool quiet{false};
50 bool show_progress{true};
51 bool list_syntaxes{false};
52};
53
57struct conversion_stats {
58 size_t total_files{0};
59 size_t success_count{0};
60 size_t skip_count{0};
61 size_t error_count{0};
62 std::chrono::milliseconds total_time{0};
63};
64
69void print_usage(const char* program_name) {
70 std::cout << R"(
71DICOM Conversion - Transfer Syntax Conversion Utility
72
73Usage: )" << program_name
74 << R"( <input> <output> [options]
75
76Arguments:
77 input Input DICOM file or directory
78 output Output DICOM file or directory
79
80Transfer Syntax Options:
81 --implicit Implicit VR Little Endian (1.2.840.10008.1.2)
82 --explicit Explicit VR Little Endian (1.2.840.10008.1.2.1) [default]
83 --explicit-be Explicit VR Big Endian (1.2.840.10008.1.2.2) [retired]
84 --jpeg-baseline JPEG Baseline (Process 1) - lossy
85 --jpeg-lossless JPEG Lossless, Non-Hierarchical
86 --jpeg2000 JPEG 2000 Image Compression (Lossless Only)
87 --jpeg2000-lossy JPEG 2000 Image Compression
88 --htj2k HTJ2K Lossless Only (1.2.840.10008.1.2.4.201)
89 --htj2k-rpcl HTJ2K with RPCL Options (1.2.840.10008.1.2.4.202)
90 --htj2k-lossy HTJ2K Lossy (1.2.840.10008.1.2.4.203)
91 --rle RLE Lossless
92 -t, --transfer-syntax <uid> Specify Transfer Syntax by UID
93
94Compression Options:
95 -q, --quality <1-100> JPEG quality (default: 90, higher = better quality)
96
97Processing Options:
98 -r, --recursive Process directory recursively
99 --overwrite Overwrite existing output files
100 --verify Verify conversion result
101 -v, --verbose Verbose output
102 -q, --quiet Minimal output (errors only)
103 --no-progress Disable progress display
104
105Information:
106 --list-syntaxes List all supported Transfer Syntaxes
107 -h, --help Show this help message
108
109Examples:
110 )" << program_name
111 << R"( image.dcm converted.dcm --explicit
112 )" << program_name
113 << R"( image.dcm compressed.dcm --jpeg-baseline -q 85
114 )" << program_name
115 << R"( ./input_dir/ ./output_dir/ --recursive --implicit
116 )" << program_name
117 << R"( image.dcm htj2k.dcm --htj2k
118 )" << program_name
119 << R"( image.dcm output.dcm -t 1.2.840.10008.1.2.4.50
120
121Exit Codes:
122 0 Success - All files converted successfully
123 1 Error - Invalid arguments or help requested
124 2 Error - Conversion failed for one or more files
125)";
126}
127
131void list_supported_syntaxes() {
132 using namespace kcenon::pacs::encoding;
133
134 std::cout << "\nSupported Transfer Syntaxes:\n";
135 std::cout << std::string(70, '-') << "\n";
136 std::cout << std::left << std::setw(40) << "Name"
137 << std::setw(30) << "UID" << "\n";
138 std::cout << std::string(70, '-') << "\n";
139
140 auto syntaxes = supported_transfer_syntaxes();
141 for (const auto& ts : syntaxes) {
142 std::cout << std::left << std::setw(40) << ts.name()
143 << std::setw(30) << ts.uid() << "\n";
144 }
145
146 std::cout << std::string(70, '-') << "\n";
147 std::cout << "Total: " << syntaxes.size() << " transfer syntaxes\n\n";
148
149 // Also list compression codecs if available
150 auto codec_uids = compression::codec_factory::supported_transfer_syntaxes();
151 if (!codec_uids.empty()) {
152 std::cout << "Compression Codecs Available:\n";
153 std::cout << std::string(50, '-') << "\n";
154 for (const auto& uid : codec_uids) {
155 if (auto ts = find_transfer_syntax(uid); ts.has_value()) {
156 std::cout << " " << ts->name() << "\n";
157 }
158 }
159 std::cout << std::string(50, '-') << "\n";
160 }
161}
162
170bool parse_arguments(int argc, char* argv[], options& opts) {
171 using namespace kcenon::pacs::encoding;
172
173 if (argc < 2) {
174 return false;
175 }
176
177 // Default to Explicit VR Little Endian
178 opts.target_transfer_syntax = std::string(transfer_syntax::explicit_vr_little_endian.uid());
179
180 for (int i = 1; i < argc; ++i) {
181 std::string arg = argv[i];
182
183 if (arg == "--help" || arg == "-h") {
184 return false;
185 } else if (arg == "--list-syntaxes") {
186 opts.list_syntaxes = true;
187 return true;
188 } else if (arg == "--implicit") {
189 opts.target_transfer_syntax = std::string(transfer_syntax::implicit_vr_little_endian.uid());
190 } else if (arg == "--explicit") {
191 opts.target_transfer_syntax = std::string(transfer_syntax::explicit_vr_little_endian.uid());
192 } else if (arg == "--explicit-be") {
193 opts.target_transfer_syntax = std::string(transfer_syntax::explicit_vr_big_endian.uid());
194 } else if (arg == "--jpeg-baseline") {
195 opts.target_transfer_syntax = std::string(transfer_syntax::jpeg_baseline.uid());
196 } else if (arg == "--jpeg-lossless") {
197 opts.target_transfer_syntax = std::string(transfer_syntax::jpeg_lossless.uid());
198 } else if (arg == "--jpeg2000") {
199 opts.target_transfer_syntax = std::string(transfer_syntax::jpeg2000_lossless.uid());
200 } else if (arg == "--jpeg2000-lossy") {
201 opts.target_transfer_syntax = std::string(transfer_syntax::jpeg2000_lossy.uid());
202 } else if (arg == "--htj2k") {
203 opts.target_transfer_syntax = std::string(transfer_syntax::htj2k_lossless.uid());
204 } else if (arg == "--htj2k-rpcl") {
205 opts.target_transfer_syntax = std::string(transfer_syntax::htj2k_rpcl.uid());
206 } else if (arg == "--htj2k-lossy") {
207 opts.target_transfer_syntax = std::string(transfer_syntax::htj2k_lossy.uid());
208 } else if (arg == "--rle") {
209 opts.target_transfer_syntax = std::string(transfer_syntax::rle_lossless.uid());
210 } else if ((arg == "-t" || arg == "--transfer-syntax") && i + 1 < argc) {
211 opts.target_transfer_syntax = argv[++i];
212 } else if ((arg == "-q" || arg == "--quality") && i + 1 < argc) {
213 try {
214 opts.quality = std::stoi(argv[++i]);
215 if (opts.quality < 1 || opts.quality > 100) {
216 std::cerr << "Error: Quality must be between 1 and 100\n";
217 return false;
218 }
219 } catch (...) {
220 std::cerr << "Error: Invalid quality value\n";
221 return false;
222 }
223 } else if (arg == "-r" || arg == "--recursive") {
224 opts.recursive = true;
225 } else if (arg == "--overwrite") {
226 opts.overwrite = true;
227 } else if (arg == "--verify") {
228 opts.verify = true;
229 } else if (arg == "-v" || arg == "--verbose") {
230 opts.verbose = true;
231 } else if (arg == "--quiet") {
232 opts.quiet = true;
233 opts.show_progress = false;
234 } else if (arg == "--no-progress") {
235 opts.show_progress = false;
236 } else if (arg[0] == '-') {
237 std::cerr << "Error: Unknown option '" << arg << "'\n";
238 return false;
239 } else if (opts.input_path.empty()) {
240 opts.input_path = arg;
241 } else if (opts.output_path.empty()) {
242 opts.output_path = arg;
243 } else {
244 std::cerr << "Error: Too many arguments\n";
245 return false;
246 }
247 }
248
249 // Validate transfer syntax
250 auto ts = find_transfer_syntax(opts.target_transfer_syntax);
251 if (!ts.has_value()) {
252 std::cerr << "Error: Unknown Transfer Syntax UID '" << opts.target_transfer_syntax << "'\n";
253 std::cerr << "Use --list-syntaxes to see available options\n";
254 return false;
255 }
256
257 if (!opts.list_syntaxes) {
258 if (opts.input_path.empty()) {
259 std::cerr << "Error: No input path specified\n";
260 return false;
261 }
262
263 if (opts.output_path.empty()) {
264 std::cerr << "Error: No output path specified\n";
265 return false;
266 }
267 }
268
269 // Quiet mode overrides verbose
270 if (opts.quiet) {
271 opts.verbose = false;
272 }
273
274 return true;
275}
276
284bool convert_file(const std::filesystem::path& input_path,
285 const std::filesystem::path& output_path,
286 const options& opts) {
287 using namespace kcenon::pacs::core;
288 using namespace kcenon::pacs::encoding;
289
290 // Check if output exists and overwrite is disabled
291 if (std::filesystem::exists(output_path) && !opts.overwrite) {
292 if (opts.verbose) {
293 std::cout << " Skipped (exists): " << output_path.filename().string() << "\n";
294 }
295 return true; // Not an error, just skipped
296 }
297
298 // Open input file
299 auto result = dicom_file::open(input_path);
300 if (result.is_err()) {
301 std::cerr << "Error: Failed to open '" << input_path.string()
302 << "': " << result.error().message << "\n";
303 return false;
304 }
305
306 auto& input_file = result.value();
307 auto source_ts = input_file.transfer_syntax();
308 auto target_ts = transfer_syntax(opts.target_transfer_syntax);
309
310 // Check if conversion is needed
311 if (source_ts == target_ts) {
312 if (opts.verbose) {
313 std::cout << " Skipped (same TS): " << input_path.filename().string() << "\n";
314 }
315 // Just copy the file
316 std::filesystem::copy_file(input_path, output_path,
317 std::filesystem::copy_options::overwrite_existing);
318 return true;
319 }
320
321 // Check if target transfer syntax is supported
322 if (!target_ts.is_supported()) {
323 std::cerr << "Error: Target Transfer Syntax '" << target_ts.name()
324 << "' is not currently supported\n";
325 return false;
326 }
327
328 if (opts.verbose) {
329 std::cout << " Converting: " << input_path.filename().string() << "\n";
330 std::cout << " From: " << source_ts.name() << "\n";
331 std::cout << " To: " << target_ts.name() << "\n";
332 }
333
334 // Create output file with new transfer syntax
335 auto output_file = dicom_file::create(
336 std::move(input_file.dataset()),
337 target_ts
338 );
339
340 // Ensure output directory exists
341 auto output_dir = output_path.parent_path();
342 if (!output_dir.empty() && !std::filesystem::exists(output_dir)) {
343 std::filesystem::create_directories(output_dir);
344 }
345
346 // Save output file
347 auto save_result = output_file.save(output_path);
348 if (save_result.is_err()) {
349 std::cerr << "Error: Failed to save '" << output_path.string()
350 << "': " << save_result.error().message << "\n";
351 return false;
352 }
353
354 // Verify if requested
355 if (opts.verify) {
356 auto verify_result = dicom_file::open(output_path);
357 if (verify_result.is_err()) {
358 std::cerr << "Error: Verification failed for '" << output_path.string()
359 << "': " << verify_result.error().message << "\n";
360 return false;
361 }
362
363 auto& verified_file = verify_result.value();
364 if (verified_file.transfer_syntax() != target_ts) {
365 std::cerr << "Error: Verification failed - Transfer Syntax mismatch\n";
366 return false;
367 }
368
369 if (opts.verbose) {
370 std::cout << " Verified: OK\n";
371 }
372 }
373
374 return true;
375}
376
384void process_directory(const std::filesystem::path& input_dir,
385 const std::filesystem::path& output_dir,
386 const options& opts,
387 conversion_stats& stats) {
388 auto process_file = [&](const std::filesystem::path& file_path) {
389 // Check file extension
390 auto ext = file_path.extension().string();
391 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
392 if (ext != ".dcm" && ext != ".dicom" && !ext.empty()) {
393 return; // Skip non-DICOM files
394 }
395
396 ++stats.total_files;
397
398 // Calculate relative path and construct output path
399 auto relative_path = std::filesystem::relative(file_path, input_dir);
400 auto output_path = output_dir / relative_path;
401
402 auto start = std::chrono::steady_clock::now();
403
404 if (convert_file(file_path, output_path, opts)) {
405 ++stats.success_count;
406 } else {
407 ++stats.error_count;
408 }
409
410 auto end = std::chrono::steady_clock::now();
411 stats.total_time += std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
412
413 if (opts.show_progress && !opts.quiet) {
414 std::cout << "\rProcessed: " << stats.total_files
415 << " (Success: " << stats.success_count
416 << ", Errors: " << stats.error_count << ")" << std::flush;
417 }
418 };
419
420 if (opts.recursive) {
421 for (const auto& entry : std::filesystem::recursive_directory_iterator(input_dir)) {
422 if (entry.is_regular_file()) {
423 process_file(entry.path());
424 }
425 }
426 } else {
427 for (const auto& entry : std::filesystem::directory_iterator(input_dir)) {
428 if (entry.is_regular_file()) {
429 process_file(entry.path());
430 }
431 }
432 }
433
434 if (opts.show_progress && !opts.quiet) {
435 std::cout << "\n"; // New line after progress
436 }
437}
438
443void print_summary(const conversion_stats& stats) {
444 std::cout << "\n";
445 std::cout << "========================================\n";
446 std::cout << " Conversion Summary\n";
447 std::cout << "========================================\n";
448 std::cout << " Total files: " << stats.total_files << "\n";
449 std::cout << " Successful: " << stats.success_count << "\n";
450 std::cout << " Skipped: " << stats.skip_count << "\n";
451 std::cout << " Errors: " << stats.error_count << "\n";
452 std::cout << " Total time: " << stats.total_time.count() << " ms\n";
453 if (stats.total_files > 0) {
454 auto avg_time = stats.total_time.count() / static_cast<double>(stats.total_files);
455 std::cout << " Avg per file: " << std::fixed << std::setprecision(1)
456 << avg_time << " ms\n";
457 }
458 std::cout << "========================================\n";
459}
460
461} // namespace
462
463int main(int argc, char* argv[]) {
464 options opts;
465
466 if (!parse_arguments(argc, argv, opts)) {
467 std::cout << R"(
468 ____ ____ __ __ ____ ___ _ ___ __
469 | _ \ / ___| \/ | / ___/ _ \| \ | \ \ / /
470 | | | | | | |\/| | | | | | | | \| |\ \ / /
471 | |_| | |___| | | | | |__| |_| | |\ | \ V /
472 |____/ \____|_| |_| \____\___/|_| \_| \_/
473
474 DICOM Transfer Syntax Converter
475)" << "\n";
476 print_usage(argv[0]);
477 return 1;
478 }
479
480 if (opts.list_syntaxes) {
481 list_supported_syntaxes();
482 return 0;
483 }
484
485 // Show banner in verbose mode
486 if (!opts.quiet) {
487 std::cout << R"(
488 ____ ____ __ __ ____ ___ _ ___ __
489 | _ \ / ___| \/ | / ___/ _ \| \ | \ \ / /
490 | | | | | | |\/| | | | | | | | \| |\ \ / /
491 | |_| | |___| | | | | |__| |_| | |\ | \ V /
492 |____/ \____|_| |_| \____\___/|_| \_| \_/
493
494 DICOM Transfer Syntax Converter
495)" << "\n";
496 }
497
498 // Check input exists
499 if (!std::filesystem::exists(opts.input_path)) {
500 std::cerr << "Error: Input path does not exist: " << opts.input_path.string() << "\n";
501 return 2;
502 }
503
504 auto target_ts = kcenon::pacs::encoding::transfer_syntax(opts.target_transfer_syntax);
505 if (!opts.quiet) {
506 std::cout << "Target Transfer Syntax: " << target_ts.name() << "\n";
507 std::cout << " UID: " << target_ts.uid() << "\n\n";
508 }
509
510 conversion_stats stats;
511 auto start_time = std::chrono::steady_clock::now();
512
513 if (std::filesystem::is_directory(opts.input_path)) {
514 // Process directory
515 if (!std::filesystem::exists(opts.output_path)) {
516 std::filesystem::create_directories(opts.output_path);
517 }
518
519 if (!opts.quiet) {
520 std::cout << "Processing directory: " << opts.input_path.string() << "\n";
521 if (opts.recursive) {
522 std::cout << "Mode: Recursive\n\n";
523 }
524 }
525
526 process_directory(opts.input_path, opts.output_path, opts, stats);
527 } else {
528 // Process single file
529 ++stats.total_files;
530
531 if (convert_file(opts.input_path, opts.output_path, opts)) {
532 ++stats.success_count;
533 } else {
534 ++stats.error_count;
535 }
536 }
537
538 auto end_time = std::chrono::steady_clock::now();
539 stats.total_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
540
541 // Print summary for directory processing
542 if (std::filesystem::is_directory(opts.input_path) && !opts.quiet) {
543 print_summary(stats);
544 } else if (!opts.quiet) {
545 if (stats.success_count > 0) {
546 std::cout << "Conversion completed successfully.\n";
547 }
548 }
549
550 return stats.error_count > 0 ? 2 : 0;
551}
Represents a DICOM Transfer Syntax.
DICOM Part 10 file handling for reading/writing DICOM files.
Compile-time constants for commonly used DICOM tags.
int main()
Definition main.cpp:84
std::optional< transfer_syntax > find_transfer_syntax(std::string_view uid)
Looks up a Transfer Syntax by its UID.
std::vector< transfer_syntax > supported_transfer_syntaxes()
Returns a list of all supported Transfer Syntaxes.
Transfer Syntax UIDs.
Definition main.cpp:78
std::string_view uid