46 std::filesystem::path input_path;
47 std::filesystem::path output_path;
48 std::filesystem::path template_path;
49 std::filesystem::path bulk_data_dir;
50 std::string transfer_syntax;
63using json_object = std::map<std::string, json_value>;
64using json_array = std::vector<json_value>;
67 std::variant<std::nullptr_t, bool, double, std::string, json_array, json_object> data;
69 bool is_null()
const {
return std::holds_alternative<std::nullptr_t>(data); }
70 bool is_bool()
const {
return std::holds_alternative<bool>(data); }
71 bool is_number()
const {
return std::holds_alternative<double>(data); }
72 bool is_string()
const {
return std::holds_alternative<std::string>(data); }
73 bool is_array()
const {
return std::holds_alternative<json_array>(data); }
74 bool is_object()
const {
return std::holds_alternative<json_object>(data); }
76 bool as_bool()
const {
return std::get<bool>(data); }
77 double as_number()
const {
return std::get<double>(data); }
78 const std::string& as_string()
const {
return std::get<std::string>(data); }
79 const json_array& as_array()
const {
return std::get<json_array>(data); }
80 const json_object& as_object()
const {
return std::get<json_object>(data); }
82 bool has(
const std::string& key)
const {
83 if (!is_object())
return false;
84 return as_object().count(key) > 0;
87 const json_value& operator[](
const std::string& key)
const {
88 return as_object().at(key);
91 const json_value& operator[](
size_t idx)
const {
92 return as_array().at(idx);
101 explicit json_parser(
const std::string& input) : input_(input),
pos_(0) {}
105 return parse_value();
109 const std::string& input_;
113 return pos_ < input_.size() ? input_[
pos_] :
'\0';
117 return pos_ < input_.size() ? input_[
pos_++] :
'\0';
120 void skip_whitespace() {
121 while (pos_ < input_.size() && std::isspace(
static_cast<unsigned char>(input_[pos_]))) {
126 void expect(
char c) {
129 throw std::runtime_error(
"Expected '" + std::string(1, c) +
"'");
133 json_value parse_value() {
137 if (c ==
'{')
return parse_object();
138 if (c ==
'[')
return parse_array();
139 if (c ==
'"')
return parse_string();
140 if (c ==
't' || c ==
'f')
return parse_bool();
141 if (c ==
'n')
return parse_null();
142 if (c ==
'-' || std::isdigit(
static_cast<unsigned char>(c)))
return parse_number();
144 throw std::runtime_error(
"Unexpected character in JSON");
147 json_value parse_object() {
154 return json_value{obj};
160 throw std::runtime_error(
"Expected string key in object");
162 std::string key = parse_string_value();
164 obj[key] = parse_value();
169 if (c !=
',')
throw std::runtime_error(
"Expected ',' or '}' in object");
172 return json_value{obj};
175 json_value parse_array() {
182 return json_value{arr};
186 arr.push_back(parse_value());
191 if (c !=
',')
throw std::runtime_error(
"Expected ',' or ']' in array");
194 return json_value{arr};
197 std::string parse_string_value() {
201 while (peek() !=
'"') {
206 case '"': result +=
'"';
break;
207 case '\\': result +=
'\\';
break;
208 case '/': result +=
'/';
break;
209 case 'b': result +=
'\b';
break;
210 case 'f': result +=
'\f';
break;
211 case 'n': result +=
'\n';
break;
212 case 'r': result +=
'\r';
break;
213 case 't': result +=
'\t';
break;
216 for (
int i = 0; i < 4; ++i) hex +=
get();
217 int code = std::stoi(hex,
nullptr, 16);
219 result +=
static_cast<char>(
code);
220 }
else if (
code < 0x800) {
221 result +=
static_cast<char>(0xC0 | (
code >> 6));
222 result +=
static_cast<char>(0x80 | (
code & 0x3F));
224 result +=
static_cast<char>(0xE0 | (
code >> 12));
225 result +=
static_cast<char>(0x80 | ((
code >> 6) & 0x3F));
226 result +=
static_cast<char>(0x80 | (
code & 0x3F));
230 default: result += c;
break;
240 json_value parse_string() {
241 return json_value{parse_string_value()};
244 json_value parse_number() {
246 if (peek() ==
'-')
get();
247 while (std::isdigit(
static_cast<unsigned char>(peek())))
get();
250 while (std::isdigit(
static_cast<unsigned char>(peek())))
get();
252 if (peek() ==
'e' || peek() ==
'E') {
254 if (peek() ==
'+' || peek() ==
'-')
get();
255 while (std::isdigit(
static_cast<unsigned char>(peek())))
get();
257 return json_value{std::stod(input_.substr(start, pos_ - start))};
260 json_value parse_bool() {
261 if (input_.substr(pos_, 4) ==
"true") {
263 return json_value{
true};
265 if (input_.substr(pos_, 5) ==
"false") {
267 return json_value{
false};
269 throw std::runtime_error(
"Invalid boolean");
272 json_value parse_null() {
273 if (input_.substr(pos_, 4) ==
"null") {
275 return json_value{
nullptr};
277 throw std::runtime_error(
"Invalid null");
288constexpr int8_t base64_decode_table[] = {
289 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
290 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
291 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
292 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
293 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
294 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
295 -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
296 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
304[[nodiscard]] std::vector<uint8_t> from_base64(
const std::string& input) {
305 std::vector<uint8_t> result;
306 result.reserve((input.size() * 3) / 4);
309 while (i < input.size()) {
311 while (i < input.size() && std::isspace(
static_cast<unsigned char>(input[i]))) ++i;
312 if (i >= input.size())
break;
314 uint32_t sextet[4] = {0, 0, 0, 0};
317 for (
int j = 0; j < 4 && i < input.size(); ++j) {
322 }
else if (
static_cast<unsigned char>(c) < 128 && base64_decode_table[
static_cast<unsigned char>(c)] >= 0) {
323 sextet[j] =
static_cast<uint32_t
>(base64_decode_table[
static_cast<unsigned char>(c)]);
330 uint32_t triple = (sextet[0] << 18) | (sextet[1] << 12) | (sextet[2] << 6) | sextet[3];
332 result.push_back(
static_cast<uint8_t
>((triple >> 16) & 0xFF));
333 if (padding < 2) result.push_back(
static_cast<uint8_t
>((triple >> 8) & 0xFF));
334 if (padding < 1) result.push_back(
static_cast<uint8_t
>(triple & 0xFF));
352 static const std::map<std::string, vr_type> vr_map = {
353 {
"AE", vr_type::AE}, {
"AS", vr_type::AS}, {
"AT", vr_type::AT},
354 {
"CS", vr_type::CS}, {
"DA", vr_type::DA}, {
"DS", vr_type::DS},
355 {
"DT", vr_type::DT}, {
"FL", vr_type::FL}, {
"FD", vr_type::FD},
356 {
"IS", vr_type::IS}, {
"LO", vr_type::LO}, {
"LT", vr_type::LT},
357 {
"OB", vr_type::OB}, {
"OD", vr_type::OD}, {
"OF", vr_type::OF},
358 {
"OL", vr_type::OL}, {
"OV", vr_type::OV}, {
"OW", vr_type::OW},
359 {
"PN", vr_type::PN}, {
"SH", vr_type::SH}, {
"SL", vr_type::SL},
360 {
"SQ", vr_type::SQ}, {
"SS", vr_type::SS}, {
"ST", vr_type::ST},
361 {
"SV", vr_type::SV}, {
"TM", vr_type::TM}, {
"UC", vr_type::UC},
362 {
"UI", vr_type::UI}, {
"UL", vr_type::UL}, {
"UN", vr_type::UN},
363 {
"UR", vr_type::UR}, {
"US", vr_type::US}, {
"UT", vr_type::UT},
367 auto it = vr_map.find(vr_str);
368 return it != vr_map.end() ? it->second : vr_type::UN;
376[[nodiscard]] std::optional<kcenon::pacs::core::dicom_tag> parse_tag(
const std::string& tag_str) {
377 if (tag_str.length() != 8) {
382 uint16_t group =
static_cast<uint16_t
>(std::stoul(tag_str.substr(0, 4),
nullptr, 16));
383 uint16_t elem =
static_cast<uint16_t
>(std::stoul(tag_str.substr(4, 4),
nullptr, 16));
395[[nodiscard]] std::string read_file(
const std::filesystem::path& path) {
396 std::ifstream file(path, std::ios::binary);
398 throw std::runtime_error(
"Cannot open file: " + path.string());
401 std::ostringstream oss;
412[[nodiscard]] std::vector<uint8_t> read_bulk_data(
const std::string& uri,
413 const std::filesystem::path& bulk_dir) {
414 std::string path = uri;
417 if (path.substr(0, 7) ==
"file://") {
418 path = path.substr(7);
422 std::filesystem::path file_path = path;
423 if (!file_path.is_absolute() && !bulk_dir.empty()) {
424 file_path = bulk_dir / file_path;
427 std::ifstream file(file_path, std::ios::binary);
429 throw std::runtime_error(
"Cannot open bulk data file: " + file_path.string());
432 return std::vector<uint8_t>(
433 std::istreambuf_iterator<char>(file),
434 std::istreambuf_iterator<char>()
439void parse_dataset(
const json_object& json_obj,
441 const options& opts);
452 const json_value& element_json,
453 const options& opts) {
457 if (!element_json.is_object()) {
458 throw std::runtime_error(
"Element value must be an object");
461 const auto& obj = element_json.as_object();
464 std::string vr_str =
"UN";
465 if (obj.count(
"vr")) {
466 vr_str = obj.at(
"vr").as_string();
468 auto vr = parse_vr(vr_str);
471 if (
vr == vr_type::SQ) {
472 dicom_element elem{tag,
vr};
473 if (obj.count(
"Value") && obj.at(
"Value").is_array()) {
474 auto& items = elem.sequence_items();
475 for (
const auto& item_json : obj.at(
"Value").as_array()) {
476 if (item_json.is_object()) {
477 dicom_dataset item_dataset;
478 parse_dataset(item_json.as_object(), item_dataset, opts);
479 items.push_back(std::move(item_dataset));
487 if (obj.count(
"InlineBinary")) {
488 std::string base64 = obj.at(
"InlineBinary").as_string();
489 auto data = from_base64(base64);
490 return dicom_element{tag,
vr, std::span<const uint8_t>(data)};
494 if (obj.count(
"BulkDataURI")) {
495 std::string uri = obj.at(
"BulkDataURI").as_string();
496 auto data = read_bulk_data(uri, opts.bulk_data_dir);
497 return dicom_element{tag,
vr, std::span<const uint8_t>(data)};
501 if (!obj.count(
"Value") || !obj.at(
"Value").is_array()) {
503 return dicom_element{tag,
vr};
506 const auto& values = obj.at(
"Value").as_array();
507 if (values.empty()) {
508 return dicom_element{tag,
vr};
512 if (
vr == vr_type::PN) {
513 std::string combined;
514 for (
size_t i = 0; i < values.size(); ++i) {
515 if (i > 0) combined +=
"\\";
516 if (values[i].is_object()) {
517 const auto& pn = values[i].as_object();
518 if (pn.count(
"Alphabetic")) {
519 combined += pn.at(
"Alphabetic").as_string();
521 }
else if (values[i].is_string()) {
522 combined += values[i].as_string();
525 return dicom_element::from_string(tag,
vr, combined);
530 std::string combined;
531 for (
size_t i = 0; i < values.size(); ++i) {
532 if (i > 0) combined +=
"\\";
533 if (values[i].is_string()) {
534 combined += values[i].as_string();
535 }
else if (values[i].is_number()) {
537 std::ostringstream oss;
538 oss << std::setprecision(17) << values[i].as_number();
539 combined += oss.str();
542 return dicom_element::from_string(tag,
vr, combined);
547 std::vector<uint8_t> data;
549 auto write_values = [&]<
typename T>() {
550 data.reserve(values.size() *
sizeof(T));
551 for (
const auto& v : values) {
554 num =
static_cast<T
>(v.as_number());
555 }
else if (v.is_string()) {
557 if constexpr (std::is_floating_point_v<T>) {
558 num =
static_cast<T
>(std::stod(v.as_string()));
560 num =
static_cast<T
>(std::stoll(v.as_string()));
563 const uint8_t* ptr =
reinterpret_cast<const uint8_t*
>(&
num);
564 data.insert(data.end(), ptr, ptr +
sizeof(T));
569 case vr_type::US: write_values.template operator()<uint16_t>();
break;
570 case vr_type::SS: write_values.template operator()<int16_t>();
break;
571 case vr_type::UL: write_values.template operator()<uint32_t>();
break;
572 case vr_type::SL: write_values.template operator()<int32_t>();
break;
573 case vr_type::FL: write_values.template operator()<
float>();
break;
574 case vr_type::FD: write_values.template operator()<
double>();
break;
575 case vr_type::UV: write_values.template operator()<uint64_t>();
break;
576 case vr_type::SV: write_values.template operator()<int64_t>();
break;
580 return dicom_element{tag,
vr, std::span<const uint8_t>(data)};
584 if (
vr == vr_type::AT) {
585 std::vector<uint8_t> data;
586 for (
const auto& v : values) {
588 auto tag_opt = parse_tag(v.as_string());
590 uint16_t group = tag_opt->group();
591 uint16_t elem = tag_opt->element();
592 data.push_back(
static_cast<uint8_t
>(group & 0xFF));
593 data.push_back(
static_cast<uint8_t
>((group >> 8) & 0xFF));
594 data.push_back(
static_cast<uint8_t
>(elem & 0xFF));
595 data.push_back(
static_cast<uint8_t
>((elem >> 8) & 0xFF));
599 return dicom_element{tag,
vr, std::span<const uint8_t>(data)};
603 if (values[0].is_string()) {
604 std::string combined;
605 for (
size_t i = 0; i < values.size(); ++i) {
606 if (i > 0) combined +=
"\\";
607 combined += values[i].as_string();
609 return dicom_element::from_string(tag,
vr, combined);
612 return dicom_element{tag,
vr};
621void parse_dataset(
const json_object& json_obj,
623 const options& opts) {
624 for (
const auto& [key, value] : json_obj) {
626 auto tag_opt = parse_tag(key);
632 auto element = create_element(*tag_opt, value, opts);
633 dataset.
insert(std::move(element));
634 }
catch (
const std::exception& e) {
636 std::cerr <<
"Warning: Failed to parse element " << key
637 <<
": " << e.what() <<
"\n";
647void print_usage(
const char* program_name) {
649JSON to DICOM Converter (DICOM PS3.18)
651Usage: )" << program_name
652 << R"( <json-file> <output-dcm> [options]
655 json-file Input JSON file (DICOM PS3.18 format)
656 output-dcm Output DICOM file
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 BulkDataURI resolution
663 -v, --verbose Verbose output
664 -q, --quiet Quiet mode (errors only)
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
673 << R"( metadata.json output.dcm
675 << R"( metadata.json output.dcm --template original.dcm
677 << R"( metadata.json output.dcm --bulk-data-dir ./bulk/
679 << R"( metadata.json output.dcm -t 1.2.840.10008.1.2
681Input Format (DICOM PS3.18 JSON):
685 "Value": [{"Alphabetic": "DOE^JOHN"}]
689 "Value": ["12345678"]
696 2 File error or invalid JSON
707bool parse_arguments(
int argc,
char* argv[], options& opts) {
712 for (
int i = 1; i < argc; ++i) {
713 std::string arg = argv[i];
715 if (arg ==
"--help" || arg ==
"-h") {
717 }
else if ((arg ==
"--transfer-syntax" || arg ==
"-t") && i + 1 < argc) {
718 opts.transfer_syntax = argv[++i];
719 }
else if (arg ==
"--template" && i + 1 < argc) {
720 opts.template_path = argv[++i];
721 }
else if (arg ==
"--bulk-data-dir" && i + 1 < argc) {
722 opts.bulk_data_dir = argv[++i];
723 }
else if (arg ==
"--verbose" || arg ==
"-v") {
725 }
else if (arg ==
"--quiet" || arg ==
"-q") {
727 }
else if (arg[0] ==
'-') {
728 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
730 }
else if (opts.input_path.empty()) {
731 opts.input_path = arg;
732 }
else if (opts.output_path.empty()) {
733 opts.output_path = arg;
735 std::cerr <<
"Error: Too many arguments\n";
740 if (opts.input_path.empty()) {
741 std::cerr <<
"Error: No input file specified\n";
745 if (opts.output_path.empty()) {
746 std::cerr <<
"Error: No output file specified\n";
751 opts.verbose =
false;
762int convert_file(
const options& opts) {
767 std::string json_content;
769 json_content = read_file(opts.input_path);
770 }
catch (
const std::exception& e) {
771 std::cerr <<
"Error: " << e.what() <<
"\n";
777 json_parser parser(json_content);
778 json = parser.parse();
779 }
catch (
const std::exception& e) {
780 std::cerr <<
"Error: Failed to parse JSON: " << e.what() <<
"\n";
784 if (!json.is_object()) {
785 std::cerr <<
"Error: JSON root must be an object\n";
790 dicom_dataset dataset;
792 parse_dataset(json.as_object(), dataset, opts);
793 }
catch (
const std::exception& e) {
794 std::cerr <<
"Error: Failed to create dataset: " << e.what() <<
"\n";
799 std::optional<dicom_file> template_file;
800 if (!opts.template_path.empty()) {
801 auto result = dicom_file::open(opts.template_path);
802 if (result.is_err()) {
803 std::cerr <<
"Error: Failed to open template file: "
804 << result.error().message <<
"\n";
807 template_file = std::move(result.value());
810 for (
const auto& [tag, element] : template_file->dataset()) {
811 if (dataset.get(tag) ==
nullptr) {
813 dataset.insert(element);
819 transfer_syntax
ts = transfer_syntax::explicit_vr_little_endian;
820 if (!opts.transfer_syntax.empty()) {
825 std::cerr <<
"Warning: Unknown transfer syntax '" << opts.transfer_syntax
826 <<
"', using Explicit VR Little Endian\n";
828 }
else if (template_file) {
829 ts = template_file->transfer_syntax();
833 auto file = dicom_file::create(dataset,
ts);
836 auto save_result = file.save(opts.output_path);
837 if (save_result.is_err()) {
838 std::cerr <<
"Error: Failed to save DICOM file: "
839 << save_result.error().message <<
"\n";
844 std::cout <<
"Successfully converted: " << opts.input_path.string()
845 <<
" -> " << opts.output_path.string() <<
"\n";
853int main(
int argc,
char* argv[]) {
856 if (!parse_arguments(argc, argv, opts)) {
858 _ ____ ___ _ _ _____ ___ ____ ____ __ __
859 | / ___| / _ \| \ | | |_ _|/ _ \ | _ \ / ___| \/ |
860 _ | \___ \| | | | \| | | | | | | | | | | | | | |\/| |
861 | |_| |___) | |_| | |\ | | | | |_| | | |_| | |___| | | |
862 \___/|____/ \___/|_| \_| |_| \___/ |____/ \____|_| |_|
864 JSON to DICOM Converter (PS3.18)
866 print_usage(argv[0]);
871 if (!std::filesystem::exists(opts.input_path)) {
872 std::cerr <<
"Error: Input file does not exist: " << opts.input_path.string() <<
"\n";
879 _ ____ ___ _ _ _____ ___ ____ ____ __ __
880 | / ___| / _ \| \ | | |_ _|/ _ \ | _ \ / ___| \/ |
881 _ | \___ \| | | | \| | | | | | | | | | | | | | |\/| |
882 | |_| |___) | |_| | |\ | | | | |_| | | |_| | |___| | | |
883 \___/|____/ \___/|_| \_| |_| \___/ |____/ \____|_| |_|
885 JSON to DICOM Converter (PS3.18)
889 return convert_file(opts);
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.
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.
vr_type
DICOM Value Representation (VR) types.
constexpr bool is_string_vr(vr_type vr) noexcept
Checks if a VR is a string type.
@ get
C-GET retrieve request/response.
@ num
NUM - Numeric measurement.