PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
25
26#include <algorithm>
27#include <cctype>
28#include <cstdint>
29#include <filesystem>
30#include <fstream>
31#include <iomanip>
32#include <iostream>
33#include <map>
34#include <optional>
35#include <sstream>
36#include <string>
37#include <vector>
38
39namespace {
40
44struct options {
45 std::filesystem::path input_path;
46 std::filesystem::path output_path;
47 std::filesystem::path template_path;
48 std::filesystem::path bulk_data_dir;
49 std::string transfer_syntax;
50 bool verbose{false};
51 bool quiet{false};
52};
53
54// ============================================================================
55// Minimal XML Parser
56// ============================================================================
57
61struct xml_node {
62 std::string name;
63 std::map<std::string, std::string> attributes;
64 std::string text;
65 std::vector<xml_node> children;
66
67 [[nodiscard]] bool has_child(const std::string& child_name) const {
68 for (const auto& child : children) {
69 if (child.name == child_name) return true;
70 }
71 return false;
72 }
73
74 [[nodiscard]] const xml_node* find_child(const std::string& child_name) const {
75 for (const auto& child : children) {
76 if (child.name == child_name) return &child;
77 }
78 return nullptr;
79 }
80
81 [[nodiscard]] std::vector<const xml_node*> find_children(const std::string& child_name) const {
82 std::vector<const xml_node*> result;
83 for (const auto& child : children) {
84 if (child.name == child_name) {
85 result.push_back(&child);
86 }
87 }
88 return result;
89 }
90
91 [[nodiscard]] std::string get_attr(const std::string& attr_name,
92 const std::string& default_value = "") const {
93 auto it = attributes.find(attr_name);
94 return it != attributes.end() ? it->second : default_value;
95 }
96};
97
101class xml_parser {
102public:
103 explicit xml_parser(const std::string& input) : input_(input), pos_(0) {}
104
105 xml_node parse() {
106 skip_whitespace();
107 skip_xml_declaration();
108 skip_whitespace();
109 return parse_element();
110 }
111
112private:
113 const std::string& input_;
114 size_t pos_;
115
116 [[nodiscard]] char peek() const {
117 return pos_ < input_.size() ? input_[pos_] : '\0';
118 }
119
120 char get() {
121 return pos_ < input_.size() ? input_[pos_++] : '\0';
122 }
123
124 void skip_whitespace() {
125 while (pos_ < input_.size() && std::isspace(static_cast<unsigned char>(input_[pos_]))) {
126 ++pos_;
127 }
128 }
129
130 void skip_xml_declaration() {
131 if (input_.substr(pos_, 5) == "<?xml") {
132 while (pos_ < input_.size() && input_.substr(pos_, 2) != "?>") {
133 ++pos_;
134 }
135 if (pos_ < input_.size()) pos_ += 2;
136 }
137 }
138
139 void skip_comment() {
140 if (input_.substr(pos_, 4) == "<!--") {
141 pos_ += 4;
142 while (pos_ < input_.size() && input_.substr(pos_, 3) != "-->") {
143 ++pos_;
144 }
145 if (pos_ < input_.size()) pos_ += 3;
146 }
147 }
148
149 [[nodiscard]] std::string parse_name() {
150 size_t start = pos_;
151 while (pos_ < input_.size() &&
152 (std::isalnum(static_cast<unsigned char>(input_[pos_])) ||
153 input_[pos_] == '_' || input_[pos_] == ':' || input_[pos_] == '-')) {
154 ++pos_;
155 }
156 return input_.substr(start, pos_ - start);
157 }
158
159 [[nodiscard]] std::string parse_attribute_value() {
160 char quote = get(); // consume opening quote
161 std::string result;
162
163 while (peek() != quote && peek() != '\0') {
164 if (peek() == '&') {
165 result += parse_entity();
166 } else {
167 result += get();
168 }
169 }
170 get(); // consume closing quote
171 return result;
172 }
173
174 [[nodiscard]] std::string parse_entity() {
175 get(); // consume '&'
176 std::string entity;
177 while (peek() != ';' && peek() != '\0') {
178 entity += get();
179 }
180 get(); // consume ';'
181
182 if (entity == "lt") return "<";
183 if (entity == "gt") return ">";
184 if (entity == "amp") return "&";
185 if (entity == "quot") return "\"";
186 if (entity == "apos") return "'";
187 if (!entity.empty() && entity[0] == '#') {
188 // Numeric entity
189 int code = 0;
190 if (entity.size() > 1 && entity[1] == 'x') {
191 code = std::stoi(entity.substr(2), nullptr, 16);
192 } else {
193 code = std::stoi(entity.substr(1));
194 }
195 return std::string(1, static_cast<char>(code));
196 }
197 return "&" + entity + ";"; // Unknown entity
198 }
199
200 [[nodiscard]] std::string parse_text() {
201 std::string result;
202 while (peek() != '<' && peek() != '\0') {
203 if (peek() == '&') {
204 result += parse_entity();
205 } else {
206 result += get();
207 }
208 }
209 // Trim whitespace
210 size_t start = result.find_first_not_of(" \t\n\r");
211 size_t end = result.find_last_not_of(" \t\n\r");
212 if (start == std::string::npos) return "";
213 return result.substr(start, end - start + 1);
214 }
215
216 xml_node parse_element() {
217 xml_node node;
218 skip_whitespace();
219
220 // Skip comments
221 while (input_.substr(pos_, 4) == "<!--") {
222 skip_comment();
223 skip_whitespace();
224 }
225
226 if (peek() != '<') {
227 throw std::runtime_error("Expected '<' at position " + std::to_string(pos_));
228 }
229 get(); // consume '<'
230
231 // Parse element name
232 node.name = parse_name();
233 skip_whitespace();
234
235 // Parse attributes
236 while (peek() != '>' && peek() != '/' && peek() != '\0') {
237 std::string attr_name = parse_name();
238 skip_whitespace();
239 if (peek() == '=') {
240 get(); // consume '='
241 skip_whitespace();
242 std::string attr_value = parse_attribute_value();
243 node.attributes[attr_name] = attr_value;
244 }
245 skip_whitespace();
246 }
247
248 // Check for self-closing tag
249 if (peek() == '/') {
250 get(); // consume '/'
251 if (peek() == '>') {
252 get(); // consume '>'
253 return node;
254 }
255 }
256
257 if (peek() != '>') {
258 throw std::runtime_error("Expected '>' at position " + std::to_string(pos_));
259 }
260 get(); // consume '>'
261
262 // Parse content
263 while (true) {
264 skip_whitespace();
265
266 // Skip comments
267 while (input_.substr(pos_, 4) == "<!--") {
268 skip_comment();
269 skip_whitespace();
270 }
271
272 if (input_.substr(pos_, 2) == "</") {
273 // End tag
274 pos_ += 2;
275 std::string end_name = parse_name();
276 skip_whitespace();
277 if (peek() == '>') get();
278 break;
279 } else if (peek() == '<') {
280 // Child element
281 node.children.push_back(parse_element());
282 } else if (peek() != '\0') {
283 // Text content
284 node.text += parse_text();
285 } else {
286 break;
287 }
288 }
289
290 return node;
291 }
292};
293
294// ============================================================================
295// Base64 Decoding
296// ============================================================================
297
298constexpr int8_t base64_decode_table[] = {
299 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
300 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
301 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
302 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
303 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
304 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
305 -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
306 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
307};
308
309[[nodiscard]] std::vector<uint8_t> from_base64(const std::string& input) {
310 std::vector<uint8_t> result;
311 result.reserve((input.size() * 3) / 4);
312
313 size_t i = 0;
314 while (i < input.size()) {
315 while (i < input.size() && std::isspace(static_cast<unsigned char>(input[i]))) ++i;
316 if (i >= input.size()) break;
317
318 uint32_t sextet[4] = {0, 0, 0, 0};
319 int padding = 0;
320
321 for (int j = 0; j < 4 && i < input.size(); ++j) {
322 char c = input[i++];
323 if (c == '=') {
324 padding++;
325 sextet[j] = 0;
326 } else if (static_cast<unsigned char>(c) < 128 &&
327 base64_decode_table[static_cast<unsigned char>(c)] >= 0) {
328 sextet[j] = static_cast<uint32_t>(base64_decode_table[static_cast<unsigned char>(c)]);
329 } else {
330 --j;
331 }
332 }
333
334 uint32_t triple = (sextet[0] << 18) | (sextet[1] << 12) | (sextet[2] << 6) | sextet[3];
335
336 result.push_back(static_cast<uint8_t>((triple >> 16) & 0xFF));
337 if (padding < 2) result.push_back(static_cast<uint8_t>((triple >> 8) & 0xFF));
338 if (padding < 1) result.push_back(static_cast<uint8_t>(triple & 0xFF));
339 }
340
341 return result;
342}
343
344// ============================================================================
345// Utility Functions
346// ============================================================================
347
348[[nodiscard]] kcenon::pacs::encoding::vr_type parse_vr(const std::string& vr_str) {
349 using namespace kcenon::pacs::encoding;
350
351 static const std::map<std::string, vr_type> vr_map = {
352 {"AE", vr_type::AE}, {"AS", vr_type::AS}, {"AT", vr_type::AT},
353 {"CS", vr_type::CS}, {"DA", vr_type::DA}, {"DS", vr_type::DS},
354 {"DT", vr_type::DT}, {"FL", vr_type::FL}, {"FD", vr_type::FD},
355 {"IS", vr_type::IS}, {"LO", vr_type::LO}, {"LT", vr_type::LT},
356 {"OB", vr_type::OB}, {"OD", vr_type::OD}, {"OF", vr_type::OF},
357 {"OL", vr_type::OL}, {"OV", vr_type::OV}, {"OW", vr_type::OW},
358 {"PN", vr_type::PN}, {"SH", vr_type::SH}, {"SL", vr_type::SL},
359 {"SQ", vr_type::SQ}, {"SS", vr_type::SS}, {"ST", vr_type::ST},
360 {"SV", vr_type::SV}, {"TM", vr_type::TM}, {"UC", vr_type::UC},
361 {"UI", vr_type::UI}, {"UL", vr_type::UL}, {"UN", vr_type::UN},
362 {"UR", vr_type::UR}, {"US", vr_type::US}, {"UT", vr_type::UT},
363 {"UV", vr_type::UV}
364 };
365
366 auto it = vr_map.find(vr_str);
367 return it != vr_map.end() ? it->second : vr_type::UN;
368}
369
370[[nodiscard]] std::optional<kcenon::pacs::core::dicom_tag> parse_tag(const std::string& tag_str) {
371 if (tag_str.length() != 8) {
372 return std::nullopt;
373 }
374
375 try {
376 uint16_t group = static_cast<uint16_t>(std::stoul(tag_str.substr(0, 4), nullptr, 16));
377 uint16_t elem = static_cast<uint16_t>(std::stoul(tag_str.substr(4, 4), nullptr, 16));
378 return kcenon::pacs::core::dicom_tag{group, elem};
379 } catch (...) {
380 return std::nullopt;
381 }
382}
383
384[[nodiscard]] std::string read_file(const std::filesystem::path& path) {
385 std::ifstream file(path, std::ios::binary);
386 if (!file) {
387 throw std::runtime_error("Cannot open file: " + path.string());
388 }
389
390 std::ostringstream oss;
391 oss << file.rdbuf();
392 return oss.str();
393}
394
395[[nodiscard]] std::vector<uint8_t> read_bulk_data(const std::string& uri,
396 const std::filesystem::path& bulk_dir) {
397 std::string path = uri;
398
399 if (path.substr(0, 7) == "file://") {
400 path = path.substr(7);
401 }
402
403 std::filesystem::path file_path = path;
404 if (!file_path.is_absolute() && !bulk_dir.empty()) {
405 file_path = bulk_dir / file_path;
406 }
407
408 std::ifstream file(file_path, std::ios::binary);
409 if (!file) {
410 throw std::runtime_error("Cannot open bulk data file: " + file_path.string());
411 }
412
413 return std::vector<uint8_t>(
414 std::istreambuf_iterator<char>(file),
415 std::istreambuf_iterator<char>()
416 );
417}
418
419// Forward declaration
420void parse_dataset(const xml_node& node,
422 const options& opts);
423
427[[nodiscard]] std::string build_person_name(const xml_node& pn_node) {
428 std::string result;
429
430 auto build_component = [](const xml_node* rep_node) -> std::string {
431 if (!rep_node) return "";
432
433 std::string family, given, middle, prefix, suffix;
434 if (auto* n = rep_node->find_child("FamilyName")) family = n->text;
435 if (auto* n = rep_node->find_child("GivenName")) given = n->text;
436 if (auto* n = rep_node->find_child("MiddleName")) middle = n->text;
437 if (auto* n = rep_node->find_child("NamePrefix")) prefix = n->text;
438 if (auto* n = rep_node->find_child("NameSuffix")) suffix = n->text;
439
440 std::string comp = family;
441 if (!given.empty() || !middle.empty() || !prefix.empty() || !suffix.empty()) {
442 comp += "^" + given;
443 }
444 if (!middle.empty() || !prefix.empty() || !suffix.empty()) {
445 comp += "^" + middle;
446 }
447 if (!prefix.empty() || !suffix.empty()) {
448 comp += "^" + prefix;
449 }
450 if (!suffix.empty()) {
451 comp += "^" + suffix;
452 }
453
454 return comp;
455 };
456
457 std::string alphabetic = build_component(pn_node.find_child("Alphabetic"));
458 std::string ideographic = build_component(pn_node.find_child("Ideographic"));
459 std::string phonetic = build_component(pn_node.find_child("Phonetic"));
460
461 result = alphabetic;
462 if (!ideographic.empty() || !phonetic.empty()) {
463 result += "=" + ideographic;
464 }
465 if (!phonetic.empty()) {
466 result += "=" + phonetic;
467 }
468
469 return result;
470}
471
475[[nodiscard]] kcenon::pacs::core::dicom_element create_element(
477 const xml_node& attr_node,
478 const options& opts) {
479 using namespace kcenon::pacs::core;
480 using namespace kcenon::pacs::encoding;
481
482 std::string vr_str = attr_node.get_attr("vr", "UN");
483 auto vr = parse_vr(vr_str);
484
485 // Handle sequence
486 if (vr == vr_type::SQ) {
487 dicom_element elem{tag, vr};
488 auto items = attr_node.find_children("Item");
489 for (const auto* item_node : items) {
490 dicom_dataset item_dataset;
491 parse_dataset(*item_node, item_dataset, opts);
492 elem.sequence_items().push_back(std::move(item_dataset));
493 }
494 return elem;
495 }
496
497 // Handle InlineBinary (Base64)
498 if (auto* inline_binary = attr_node.find_child("InlineBinary")) {
499 auto data = from_base64(inline_binary->text);
500 return dicom_element{tag, vr, std::span<const uint8_t>(data)};
501 }
502
503 // Handle BulkData URI
504 if (auto* bulk_data = attr_node.find_child("BulkData")) {
505 std::string uri = bulk_data->get_attr("uri");
506 if (!uri.empty()) {
507 auto data = read_bulk_data(uri, opts.bulk_data_dir);
508 return dicom_element{tag, vr, std::span<const uint8_t>(data)};
509 }
510 }
511
512 // Handle PersonName
513 if (vr == vr_type::PN) {
514 auto pn_nodes = attr_node.find_children("PersonName");
515 if (!pn_nodes.empty()) {
516 std::string combined;
517 for (size_t i = 0; i < pn_nodes.size(); ++i) {
518 if (i > 0) combined += "\\";
519 combined += build_person_name(*pn_nodes[i]);
520 }
521 return dicom_element::from_string(tag, vr, combined);
522 }
523 }
524
525 // Handle Value elements
526 auto value_nodes = attr_node.find_children("Value");
527 if (value_nodes.empty()) {
528 return dicom_element{tag, vr};
529 }
530
531 // Collect values sorted by number attribute
532 std::vector<std::pair<int, std::string>> numbered_values;
533 for (const auto* val_node : value_nodes) {
534 int num = 1;
535 try {
536 num = std::stoi(val_node->get_attr("number", "1"));
537 } catch (...) {}
538 numbered_values.emplace_back(num, val_node->text);
539 }
540 std::sort(numbered_values.begin(), numbered_values.end(),
541 [](const auto& a, const auto& b) { return a.first < b.first; });
542
543 // Handle string VRs
544 if (is_string_vr(vr)) {
545 std::string combined;
546 for (size_t i = 0; i < numbered_values.size(); ++i) {
547 if (i > 0) combined += "\\";
548 combined += numbered_values[i].second;
549 }
550 return dicom_element::from_string(tag, vr, combined);
551 }
552
553 // Handle numeric VRs
554 if (is_numeric_vr(vr)) {
555 std::vector<uint8_t> data;
556
557 auto write_values = [&]<typename T>() {
558 for (const auto& [num, val_str] : numbered_values) {
559 T num_val{};
560 try {
561 if constexpr (std::is_floating_point_v<T>) {
562 num_val = static_cast<T>(std::stod(val_str));
563 } else {
564 num_val = static_cast<T>(std::stoll(val_str));
565 }
566 } catch (...) {}
567 const uint8_t* ptr = reinterpret_cast<const uint8_t*>(&num_val);
568 data.insert(data.end(), ptr, ptr + sizeof(T));
569 }
570 };
571
572 switch (vr) {
573 case vr_type::US: write_values.template operator()<uint16_t>(); break;
574 case vr_type::SS: write_values.template operator()<int16_t>(); break;
575 case vr_type::UL: write_values.template operator()<uint32_t>(); break;
576 case vr_type::SL: write_values.template operator()<int32_t>(); break;
577 case vr_type::FL: write_values.template operator()<float>(); break;
578 case vr_type::FD: write_values.template operator()<double>(); break;
579 case vr_type::UV: write_values.template operator()<uint64_t>(); break;
580 case vr_type::SV: write_values.template operator()<int64_t>(); break;
581 default: break;
582 }
583
584 return dicom_element{tag, vr, std::span<const uint8_t>(data)};
585 }
586
587 // Handle AT (Attribute Tag)
588 if (vr == vr_type::AT) {
589 std::vector<uint8_t> data;
590 for (const auto& [num, val_str] : numbered_values) {
591 auto tag_opt = parse_tag(val_str);
592 if (tag_opt) {
593 uint16_t group = tag_opt->group();
594 uint16_t elem = tag_opt->element();
595 data.push_back(static_cast<uint8_t>(group & 0xFF));
596 data.push_back(static_cast<uint8_t>((group >> 8) & 0xFF));
597 data.push_back(static_cast<uint8_t>(elem & 0xFF));
598 data.push_back(static_cast<uint8_t>((elem >> 8) & 0xFF));
599 }
600 }
601 return dicom_element{tag, vr, std::span<const uint8_t>(data)};
602 }
603
604 // Fallback: treat as string
605 if (!numbered_values.empty()) {
606 std::string combined;
607 for (size_t i = 0; i < numbered_values.size(); ++i) {
608 if (i > 0) combined += "\\";
609 combined += numbered_values[i].second;
610 }
611 return dicom_element::from_string(tag, vr, combined);
612 }
613
614 return dicom_element{tag, vr};
615}
616
620void parse_dataset(const xml_node& node,
622 const options& opts) {
623 for (const auto& child : node.children) {
624 if (child.name != "DicomAttribute") continue;
625
626 std::string tag_str = child.get_attr("tag");
627 auto tag_opt = parse_tag(tag_str);
628 if (!tag_opt) {
629 if (opts.verbose) {
630 std::cerr << "Warning: Invalid tag '" << tag_str << "', skipping\n";
631 }
632 continue;
633 }
634
635 try {
636 auto element = create_element(*tag_opt, child, opts);
637 dataset.insert(std::move(element));
638 } catch (const std::exception& e) {
639 if (opts.verbose) {
640 std::cerr << "Warning: Failed to parse element " << tag_str
641 << ": " << e.what() << "\n";
642 }
643 }
644 }
645}
646
647void print_usage(const char* program_name) {
648 std::cout << R"(
649XML to DICOM Converter (DICOM Native XML PS3.19)
650
651Usage: )" << program_name
652 << R"( <xml-file> <output-dcm> [options]
653
654Arguments:
655 xml-file Input XML file (DICOM Native XML PS3.19 format)
656 output-dcm Output DICOM file
657
658Options:
659 -h, --help Show this help message
660 -t, --transfer-syntax Transfer Syntax UID (default: Explicit VR Little Endian)
661 --template <dcm> Template DICOM file (copies pixel data and missing tags)
662 --bulk-data-dir <dir> Directory for BulkData URI resolution
663 -v, --verbose Verbose output
664 -q, --quiet Quiet mode (errors only)
665
666Transfer Syntax Options:
667 1.2.840.10008.1.2 Implicit VR Little Endian
668 1.2.840.10008.1.2.1 Explicit VR Little Endian (default)
669 1.2.840.10008.1.2.2 Explicit VR Big Endian
670
671Examples:
672 )" << program_name
673 << R"( metadata.xml output.dcm
674 )" << program_name
675 << R"( metadata.xml output.dcm --template original.dcm
676 )" << program_name
677 << R"( metadata.xml output.dcm --bulk-data-dir ./bulk/
678 )" << program_name
679 << R"( metadata.xml output.dcm -t 1.2.840.10008.1.2
680
681Input Format (DICOM Native XML PS3.19):
682 <?xml version="1.0" encoding="UTF-8"?>
683 <NativeDicomModel>
684 <DicomAttribute tag="00100010" vr="PN" keyword="PatientName">
685 <PersonName>
686 <Alphabetic>
687 <FamilyName>DOE</FamilyName>
688 <GivenName>JOHN</GivenName>
689 </Alphabetic>
690 </PersonName>
691 </DicomAttribute>
692 </NativeDicomModel>
693
694Exit Codes:
695 0 Success
696 1 Invalid arguments
697 2 File error or invalid XML
698)";
699}
700
701bool parse_arguments(int argc, char* argv[], options& opts) {
702 if (argc < 3) {
703 return false;
704 }
705
706 for (int i = 1; i < argc; ++i) {
707 std::string arg = argv[i];
708
709 if (arg == "--help" || arg == "-h") {
710 return false;
711 } else if ((arg == "--transfer-syntax" || arg == "-t") && i + 1 < argc) {
712 opts.transfer_syntax = argv[++i];
713 } else if (arg == "--template" && i + 1 < argc) {
714 opts.template_path = argv[++i];
715 } else if (arg == "--bulk-data-dir" && i + 1 < argc) {
716 opts.bulk_data_dir = argv[++i];
717 } else if (arg == "--verbose" || arg == "-v") {
718 opts.verbose = true;
719 } else if (arg == "--quiet" || arg == "-q") {
720 opts.quiet = true;
721 } else if (arg[0] == '-') {
722 std::cerr << "Error: Unknown option '" << arg << "'\n";
723 return false;
724 } else if (opts.input_path.empty()) {
725 opts.input_path = arg;
726 } else if (opts.output_path.empty()) {
727 opts.output_path = arg;
728 } else {
729 std::cerr << "Error: Too many arguments\n";
730 return false;
731 }
732 }
733
734 if (opts.input_path.empty()) {
735 std::cerr << "Error: No input file specified\n";
736 return false;
737 }
738
739 if (opts.output_path.empty()) {
740 std::cerr << "Error: No output file specified\n";
741 return false;
742 }
743
744 if (opts.quiet) {
745 opts.verbose = false;
746 }
747
748 return true;
749}
750
751int convert_file(const options& opts) {
752 using namespace kcenon::pacs::core;
753 using namespace kcenon::pacs::encoding;
754
755 // Read and parse XML
756 std::string xml_content;
757 try {
758 xml_content = read_file(opts.input_path);
759 } catch (const std::exception& e) {
760 std::cerr << "Error: " << e.what() << "\n";
761 return 2;
762 }
763
764 xml_node root;
765 try {
766 xml_parser parser(xml_content);
767 root = parser.parse();
768 } catch (const std::exception& e) {
769 std::cerr << "Error: Failed to parse XML: " << e.what() << "\n";
770 return 2;
771 }
772
773 if (root.name != "NativeDicomModel") {
774 std::cerr << "Error: XML root element must be 'NativeDicomModel', got '"
775 << root.name << "'\n";
776 return 2;
777 }
778
779 // Create dataset from XML
780 dicom_dataset dataset;
781 try {
782 parse_dataset(root, dataset, opts);
783 } catch (const std::exception& e) {
784 std::cerr << "Error: Failed to create dataset: " << e.what() << "\n";
785 return 2;
786 }
787
788 // Load template if specified
789 std::optional<dicom_file> template_file;
790 if (!opts.template_path.empty()) {
791 auto result = dicom_file::open(opts.template_path);
792 if (result.is_err()) {
793 std::cerr << "Error: Failed to open template file: "
794 << result.error().message << "\n";
795 return 2;
796 }
797 template_file = std::move(result.value());
798
799 // Merge template data (XML values override template)
800 for (const auto& [tag, element] : template_file->dataset()) {
801 if (dataset.get(tag) == nullptr) {
802 dataset.insert(element);
803 }
804 }
805 }
806
807 // Determine transfer syntax
808 transfer_syntax ts = transfer_syntax::explicit_vr_little_endian;
809 if (!opts.transfer_syntax.empty()) {
810 auto ts_opt = find_transfer_syntax(opts.transfer_syntax);
811 if (ts_opt) {
812 ts = *ts_opt;
813 } else {
814 std::cerr << "Warning: Unknown transfer syntax '" << opts.transfer_syntax
815 << "', using Explicit VR Little Endian\n";
816 }
817 } else if (template_file) {
818 ts = template_file->transfer_syntax();
819 }
820
821 // Create DICOM file
822 auto file = dicom_file::create(dataset, ts);
823
824 // Save to output
825 auto save_result = file.save(opts.output_path);
826 if (save_result.is_err()) {
827 std::cerr << "Error: Failed to save DICOM file: "
828 << save_result.error().message << "\n";
829 return 2;
830 }
831
832 if (!opts.quiet) {
833 std::cout << "Successfully converted: " << opts.input_path.string()
834 << " -> " << opts.output_path.string() << "\n";
835 }
836
837 return 0;
838}
839
840} // namespace
841
842int main(int argc, char* argv[]) {
843 options opts;
844
845 if (!parse_arguments(argc, argv, opts)) {
846 std::cout << R"(
847 __ ____ __ _ _____ ___ ____ ____ __ __
848 \ \/ / \/ | | |_ _|/ _ \ | _ \ / ___| \/ |
849 \ /| |\/| | | | | | | | | | | | | | | |\/| |
850 / \| | | | |___ | | | |_| | | |_| | |___| | | |
851 /_/\_\_| |_|_____| |_| \___/ |____/ \____|_| |_|
852
853 XML to DICOM Converter (PS3.19)
854)" << "\n";
855 print_usage(argv[0]);
856 return 1;
857 }
858
859 // Check if input path exists
860 if (!std::filesystem::exists(opts.input_path)) {
861 std::cerr << "Error: Input file does not exist: " << opts.input_path.string() << "\n";
862 return 2;
863 }
864
865 // Show banner in non-quiet mode
866 if (!opts.quiet) {
867 std::cout << R"(
868 __ ____ __ _ _____ ___ ____ ____ __ __
869 \ \/ / \/ | | |_ _|/ _ \ | _ \ / ___| \/ |
870 \ /| |\/| | | | | | | | | | | | | | | |\/| |
871 / \| | | | |___ | | | |_| | | |_| | |___| | | |
872 /_/\_\_| |_|_____| |_| \___/ |____/ \____|_| |_|
873
874 XML to DICOM Converter (PS3.19)
875)" << "\n";
876 }
877
878 return convert_file(opts);
879}
void insert(dicom_element element)
Insert or replace an element in the dataset.
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.
size_t pos_
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.
constexpr bool is_numeric_vr(vr_type vr) noexcept
Checks if a VR is a numeric type.
Definition vr_type.h:214
vr_type
DICOM Value Representation (VR) types.
Definition vr_type.h:29
constexpr bool is_string_vr(vr_type vr) noexcept
Checks if a VR is a string type.
Definition vr_type.h:175
@ get
C-GET retrieve request/response.
Transfer Syntax UIDs.
Definition main.cpp:78
std::string_view code
vr_encoding vr
std::string_view name