PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::web::dicomweb Namespace Reference

Classes

struct  accept_info
 Parsed Accept header information. More...
 
struct  media_type
 Media types supported by WADO-RS. More...
 
class  multipart_builder
 Builder for multipart MIME responses. More...
 
class  multipart_parser
 Parser for multipart/related request bodies. More...
 
struct  multipart_part
 Parsed part from multipart request. More...
 
struct  rendered_params
 Parameters for rendered image requests. More...
 
struct  rendered_result
 Result of rendered image operation. More...
 
struct  store_instance_result
 STOW-RS store result for a single instance. More...
 
struct  store_response
 STOW-RS overall store response. More...
 
struct  validation_result
 Validation result for DICOM instance. More...
 

Enumerations

enum class  rendered_format { jpeg , png , jphc }
 Rendered image output format. More...
 

Functions

auto parse_accept_header (std::string_view accept_header) -> std::vector< accept_info >
 Parse Accept header into structured format.
 
auto is_acceptable (const std::vector< accept_info > &accept_infos, std::string_view media_type) -> bool
 Check if a media type is acceptable based on Accept header.
 
auto dataset_to_dicom_json (const core::dicom_dataset &dataset, bool include_bulk_data=false, std::string_view bulk_data_uri_prefix="") -> std::string
 Convert a DICOM dataset to DicomJSON format.
 
auto vr_to_string (uint16_t vr_code) -> std::string
 Convert a VR type code to DicomJSON VR string.
 
auto is_bulk_data_tag (uint32_t tag) -> bool
 Check if a DICOM tag contains bulk data.
 
auto validate_instance (const core::dicom_dataset &dataset, std::optional< std::string_view > target_study_uid=std::nullopt) -> validation_result
 Validate a DICOM instance for STOW-RS storage.
 
auto build_store_response_json (const store_response &response, std::string_view base_url) -> std::string
 Build STOW-RS response in DicomJSON format.
 
auto study_record_to_dicom_json (const storage::study_record &record, std::string_view patient_id, std::string_view patient_name) -> std::string
 Convert a study record to DicomJSON format for QIDO-RS response.
 
auto series_record_to_dicom_json (const storage::series_record &record, std::string_view study_uid) -> std::string
 Convert a series record to DicomJSON format for QIDO-RS response.
 
auto instance_record_to_dicom_json (const storage::instance_record &record, std::string_view series_uid, std::string_view study_uid) -> std::string
 Convert an instance record to DicomJSON format for QIDO-RS response.
 
auto parse_study_query_params (const std::string &url_params) -> storage::study_query
 Parse QIDO-RS query parameters from HTTP request.
 
auto parse_series_query_params (const std::string &url_params) -> storage::series_query
 Parse QIDO-RS series query parameters from HTTP request.
 
auto parse_instance_query_params (const std::string &url_params) -> storage::instance_query
 Parse QIDO-RS instance query parameters from HTTP request.
 
auto parse_frame_numbers (std::string_view frame_list) -> std::vector< uint32_t >
 Parse frame numbers from URL path.
 
auto extract_frame (std::span< const uint8_t > pixel_data, uint32_t frame_number, size_t frame_size) -> std::vector< uint8_t >
 Extract a single frame from pixel data.
 
auto parse_rendered_params (std::string_view query_string, std::string_view accept_header) -> rendered_params
 Parse rendered image parameters from HTTP request.
 
auto apply_window_level (std::span< const uint8_t > pixel_data, uint16_t width, uint16_t height, uint16_t bits_stored, bool is_signed, double window_center, double window_width, double rescale_slope=1.0, double rescale_intercept=0.0) -> std::vector< uint8_t >
 Apply window/level transformation to pixel data.
 
auto render_dicom_image (std::string_view file_path, const rendered_params &params) -> rendered_result
 Render a DICOM image to JPEG or PNG.
 

Enumeration Type Documentation

◆ rendered_format

Rendered image output format.

Enumerator
jpeg 

JPEG format (default)

png 

PNG format.

jphc 

HTJ2K format (image/jphc)

Definition at line 456 of file dicomweb_endpoints.h.

456 {
457 jpeg,
458 png,
459 jphc
460};

Function Documentation

◆ apply_window_level()

auto kcenon::pacs::web::dicomweb::apply_window_level ( std::span< const uint8_t > pixel_data,
uint16_t width,
uint16_t height,
uint16_t bits_stored,
bool is_signed,
double window_center,
double window_width,
double rescale_slope = 1.0,
double rescale_intercept = 0.0 ) -> std::vector<uint8_t>
nodiscard

Apply window/level transformation to pixel data.

Parameters
pixel_dataRaw pixel values (16-bit)
widthImage width
heightImage height
bits_storedBits stored per pixel
is_signedWhether pixel values are signed
window_centerWindow center value
window_widthWindow width value
rescale_slopeRescale slope (default 1.0)
rescale_interceptRescale intercept (default 0.0)
Returns
8-bit grayscale image data

Definition at line 1264 of file dicomweb_endpoints.cpp.

1273 {
1274
1275 std::vector<uint8_t> output(static_cast<size_t>(width) * height);
1276
1277 // Calculate window boundaries
1278 double window_min = window_center - window_width / 2.0;
1279 double window_max = window_center + window_width / 2.0;
1280
1281 size_t pixel_count = static_cast<size_t>(width) * height;
1282 bool is_16bit = (bits_stored > 8);
1283
1284 for (size_t i = 0; i < pixel_count; ++i) {
1285 double value;
1286
1287 if (is_16bit) {
1288 // 16-bit pixel data
1289 size_t byte_offset = i * 2;
1290 if (byte_offset + 1 >= pixel_data.size()) break;
1291
1292 if (is_signed) {
1293 int16_t raw_value = static_cast<int16_t>(
1294 pixel_data[byte_offset] |
1295 (pixel_data[byte_offset + 1] << 8));
1296 value = raw_value * rescale_slope + rescale_intercept;
1297 } else {
1298 uint16_t raw_value = static_cast<uint16_t>(
1299 pixel_data[byte_offset] |
1300 (pixel_data[byte_offset + 1] << 8));
1301 value = raw_value * rescale_slope + rescale_intercept;
1302 }
1303 } else {
1304 // 8-bit pixel data
1305 if (i >= pixel_data.size()) break;
1306
1307 if (is_signed) {
1308 value = static_cast<int8_t>(pixel_data[i]) * rescale_slope +
1309 rescale_intercept;
1310 } else {
1311 value = pixel_data[i] * rescale_slope + rescale_intercept;
1312 }
1313 }
1314
1315 // Apply window/level
1316 uint8_t output_value;
1317 if (value <= window_min) {
1318 output_value = 0;
1319 } else if (value >= window_max) {
1320 output_value = 255;
1321 } else {
1322 output_value = static_cast<uint8_t>(
1323 (value - window_min) / window_width * 255.0);
1324 }
1325
1326 output[i] = output_value;
1327 }
1328
1329 return output;
1330}
constexpr dicom_tag window_width
Window Width.

Referenced by render_dicom_image().

Here is the caller graph for this function:

◆ build_store_response_json()

auto kcenon::pacs::web::dicomweb::build_store_response_json ( const store_response & response,
std::string_view base_url ) -> std::string
nodiscard

Build STOW-RS response in DicomJSON format.

Parameters
responseThe store response
base_urlBase URL for retrieve URLs
Returns
DicomJSON response body

Definition at line 601 of file dicomweb_endpoints.cpp.

603 {
604 std::ostringstream oss;
605 oss << "{";
606
607 // Referenced SOP Sequence (0008,1199) - successfully stored instances
608 if (!response.referenced_instances.empty()) {
609 oss << "\"00081199\":{\"vr\":\"SQ\",\"Value\":[";
610 bool first = true;
611 for (const auto& inst : response.referenced_instances) {
612 if (!first) oss << ",";
613 first = false;
614
615 oss << "{";
616 // Referenced SOP Class UID (0008,1150)
617 oss << "\"00081150\":{\"vr\":\"UI\",\"Value\":[\""
618 << inst.sop_class_uid << "\"]},";
619 // Referenced SOP Instance UID (0008,1155)
620 oss << "\"00081155\":{\"vr\":\"UI\",\"Value\":[\""
621 << inst.sop_instance_uid << "\"]},";
622 // Retrieve URL (0008,1190)
623 oss << "\"00081190\":{\"vr\":\"UR\",\"Value\":[\""
624 << base_url << inst.retrieve_url << "\"]}";
625 oss << "}";
626 }
627 oss << "]}";
628 }
629
630 // Failed SOP Sequence (0008,1198) - failed instances
631 if (!response.failed_instances.empty()) {
632 if (!response.referenced_instances.empty()) {
633 oss << ",";
634 }
635 oss << "\"00081198\":{\"vr\":\"SQ\",\"Value\":[";
636 bool first = true;
637 for (const auto& inst : response.failed_instances) {
638 if (!first) oss << ",";
639 first = false;
640
641 oss << "{";
642 // Referenced SOP Class UID (0008,1150)
643 if (!inst.sop_class_uid.empty()) {
644 oss << "\"00081150\":{\"vr\":\"UI\",\"Value\":[\""
645 << inst.sop_class_uid << "\"]},";
646 }
647 // Referenced SOP Instance UID (0008,1155)
648 if (!inst.sop_instance_uid.empty()) {
649 oss << "\"00081155\":{\"vr\":\"UI\",\"Value\":[\""
650 << inst.sop_instance_uid << "\"]},";
651 }
652 // Failure Reason (0008,1197)
653 uint16_t failure_reason = 272; // Processing failure
654 if (inst.error_code && *inst.error_code == "DUPLICATE") {
655 failure_reason = 273; // Duplicate SOP Instance
656 } else if (inst.error_code && *inst.error_code == "INVALID_DATA") {
657 failure_reason = 272; // Processing failure
658 }
659 oss << "\"00081197\":{\"vr\":\"US\",\"Value\":["
660 << failure_reason << "]}";
661 oss << "}";
662 }
663 oss << "]}";
664 }
665
666 oss << "}";
667 return oss.str();
668}
constexpr dicom_tag failure_reason
Failure Reason — reason code for commitment failure (PS3.4 Table J.3-2)
std::vector< store_instance_result > referenced_instances
Successfully stored.
std::vector< store_instance_result > failed_instances
Failed to store.

◆ dataset_to_dicom_json()

auto kcenon::pacs::web::dicomweb::dataset_to_dicom_json ( const core::dicom_dataset & dataset,
bool include_bulk_data = false,
std::string_view bulk_data_uri_prefix = "" ) -> std::string
nodiscard

Convert a DICOM dataset to DicomJSON format.

Parameters
datasetThe DICOM dataset to convert
include_bulk_dataWhether to include bulk data inline
bulk_data_uri_prefixURI prefix for bulk data references
Returns
DicomJSON string

Definition at line 670 of file dicomweb_endpoints.cpp.

673 {
674 std::ostringstream oss;
675 oss << "{";
676
677 bool first = true;
678 // Iterate using begin()/end() - dataset is iterable as map<dicom_tag, dicom_element>
679 for (const auto& [tag_key, elem] : dataset) {
680 if (!first) {
681 oss << ",";
682 }
683 first = false;
684
685 // Format tag as 8-digit hex
686 uint32_t tag = tag_key.combined();
687 oss << "\"";
688 oss << std::hex << std::setfill('0') << std::setw(8) << tag;
689 oss << std::dec << "\":{";
690
691 // Add VR
692 std::string vr_str = vr_enum_to_string(elem.vr());
693 oss << "\"vr\":\"" << vr_str << "\"";
694
695 // Check if bulk data
696 if (is_bulk_data_tag(tag) && !include_bulk_data) {
697 if (!bulk_data_uri_prefix.empty()) {
698 oss << ",\"BulkDataURI\":\"" << bulk_data_uri_prefix
699 << std::hex << std::setfill('0') << std::setw(8) << tag
700 << std::dec << "\"";
701 }
702 } else {
703 // Add Value based on VR type
704 auto value_str = elem.as_string().unwrap_or("");
705 if (!value_str.empty()) {
706 oss << ",\"Value\":[";
707
708 // Handle different VR types
709 using vr_type = kcenon::pacs::encoding::vr_type;
710 switch (elem.vr()) {
711 case vr_type::PN:
712 // Person Name - object with Alphabetic component
713 oss << "{\"Alphabetic\":\"" << json_escape(value_str) << "\"}";
714 break;
715
716 case vr_type::IS:
717 case vr_type::SL:
718 case vr_type::SS:
719 case vr_type::UL:
720 case vr_type::US:
721 // Integer types - output as number
722 oss << value_str;
723 break;
724
725 case vr_type::DS:
726 case vr_type::FL:
727 case vr_type::FD:
728 // Decimal types - output as number
729 oss << value_str;
730 break;
731
732 default:
733 // String types
734 oss << "\"" << json_escape(value_str) << "\"";
735 break;
736 }
737
738 oss << "]";
739 }
740 }
741
742 oss << "}";
743 }
744
745 oss << "}";
746 return oss.str();
747}
vr_type
DICOM Value Representation (VR) types.
Definition vr_type.h:29

References is_bulk_data_tag(), and kcenon::pacs::web::json_escape().

Here is the call graph for this function:

◆ extract_frame()

auto kcenon::pacs::web::dicomweb::extract_frame ( std::span< const uint8_t > pixel_data,
uint32_t frame_number,
size_t frame_size ) -> std::vector<uint8_t>
nodiscard

Extract a single frame from pixel data.

Parameters
pixel_dataComplete pixel data buffer
frame_numberFrame number to extract (1-based)
frame_sizeSize of each frame in bytes
Returns
Frame data, or empty vector if frame doesn't exist

Definition at line 1176 of file dicomweb_endpoints.cpp.

1179 {
1180
1181 if (frame_number == 0 || frame_size == 0) {
1182 return {};
1183 }
1184
1185 // Frame numbers are 1-based
1186 size_t offset = (frame_number - 1) * frame_size;
1187
1188 if (offset + frame_size > pixel_data.size()) {
1189 return {}; // Frame doesn't exist
1190 }
1191
1192 return std::vector<uint8_t>(
1193 pixel_data.begin() + static_cast<ptrdiff_t>(offset),
1194 pixel_data.begin() + static_cast<ptrdiff_t>(offset + frame_size));
1195}

Referenced by render_dicom_image().

Here is the caller graph for this function:

◆ instance_record_to_dicom_json()

auto kcenon::pacs::web::dicomweb::instance_record_to_dicom_json ( const storage::instance_record & record,
std::string_view series_uid,
std::string_view study_uid ) -> std::string
nodiscard

Convert an instance record to DicomJSON format for QIDO-RS response.

Parameters
recordThe instance record to convert
series_uidThe Series Instance UID for this instance
study_uidThe Study Instance UID for this instance
Returns
DicomJSON string for the instance

Definition at line 882 of file dicomweb_endpoints.cpp.

885 {
886 std::ostringstream oss;
887 oss << "{";
888
889 // SOP Class UID (0008,0016)
890 if (!record.sop_class_uid.empty()) {
891 oss << "\"00080016\":{\"vr\":\"UI\",\"Value\":[\""
892 << json_escape(record.sop_class_uid) << "\"]}";
893 }
894
895 // SOP Instance UID (0008,0018)
896 oss << ",\"00080018\":{\"vr\":\"UI\",\"Value\":[\""
897 << json_escape(record.sop_uid) << "\"]}";
898
899 // Study Instance UID (0020,000D)
900 if (!study_uid.empty()) {
901 oss << ",\"0020000D\":{\"vr\":\"UI\",\"Value\":[\""
902 << json_escape(std::string(study_uid)) << "\"]}";
903 }
904
905 // Series Instance UID (0020,000E)
906 if (!series_uid.empty()) {
907 oss << ",\"0020000E\":{\"vr\":\"UI\",\"Value\":[\""
908 << json_escape(std::string(series_uid)) << "\"]}";
909 }
910
911 // Instance Number (0020,0013)
912 if (record.instance_number.has_value()) {
913 oss << ",\"00200013\":{\"vr\":\"IS\",\"Value\":["
914 << *record.instance_number << "]}";
915 }
916
917 // Rows (0028,0010)
918 if (record.rows.has_value()) {
919 oss << ",\"00280010\":{\"vr\":\"US\",\"Value\":["
920 << *record.rows << "]}";
921 }
922
923 // Columns (0028,0011)
924 if (record.columns.has_value()) {
925 oss << ",\"00280011\":{\"vr\":\"US\",\"Value\":["
926 << *record.columns << "]}";
927 }
928
929 // Number of Frames (0028,0008)
930 if (record.number_of_frames.has_value()) {
931 oss << ",\"00280008\":{\"vr\":\"IS\",\"Value\":["
932 << *record.number_of_frames << "]}";
933 }
934
935 oss << "}";
936 return oss.str();
937}

References kcenon::pacs::web::json_escape().

Here is the call graph for this function:

◆ is_acceptable()

auto kcenon::pacs::web::dicomweb::is_acceptable ( const std::vector< accept_info > & accept_infos,
std::string_view media_type ) -> bool
nodiscard

Check if a media type is acceptable based on Accept header.

Parameters
accept_infosParsed accept header
media_typeThe media type to check
Returns
true if acceptable

Definition at line 184 of file dicomweb_endpoints.cpp.

185 {
186 if (accept_infos.empty()) {
187 return true; // Accept all if no Accept header
188 }
189
190 for (const auto& info : accept_infos) {
191 if (info.media_type == "*/*" ||
192 info.media_type == media_type ||
193 (info.media_type.ends_with("/*") &&
194 media_type.starts_with(
195 info.media_type.substr(0, info.media_type.size() - 1)))) {
196 return true;
197 }
198 }
199 return false;
200}

◆ is_bulk_data_tag()

auto kcenon::pacs::web::dicomweb::is_bulk_data_tag ( uint32_t tag) -> bool
nodiscard

Check if a DICOM tag contains bulk data.

Parameters
tagThe DICOM tag
Returns
true if this is a bulk data tag

Definition at line 295 of file dicomweb_endpoints.cpp.

295 {
296 // Pixel Data and related bulk data tags
297 uint16_t group = static_cast<uint16_t>(tag >> 16);
298
299 // Audio Sample Data is in group 0x50xx (curve data)
300 if (group >= 0x5000 && group <= 0x50FF) {
301 uint16_t element = static_cast<uint16_t>(tag & 0xFFFF);
302 if (element == 0x3000) {
303 return true; // Audio Sample Data
304 }
305 }
306
307 return tag == 0x7FE00010 || // Pixel Data
308 tag == 0x7FE00008 || // Float Pixel Data
309 tag == 0x7FE00009 || // Double Float Pixel Data
310 tag == 0x00420011 || // Encapsulated Document
311 tag == 0x00660023 || // Triangle Point Index List
312 tag == 0x00660024 || // Edge Point Index List
313 tag == 0x00660025 || // Vertex Point Index List
314 tag == 0x00660026 || // Triangle Strip Sequence
315 tag == 0x00660027 || // Triangle Fan Sequence
316 tag == 0x00660028 || // Line Sequence
317 tag == 0x00660029; // Primitive Point Index List
318}

Referenced by dataset_to_dicom_json().

Here is the caller graph for this function:

◆ parse_accept_header()

auto kcenon::pacs::web::dicomweb::parse_accept_header ( std::string_view accept_header) -> std::vector<accept_info>
nodiscard

Parse Accept header into structured format.

Parameters
accept_headerThe raw Accept header value
Returns
Vector of parsed accept info, sorted by quality

Definition at line 132 of file dicomweb_endpoints.cpp.

133 {
134 std::vector<accept_info> result;
135
136 if (accept_header.empty()) {
137 // Default to application/dicom
138 result.push_back({std::string(media_type::dicom), "", 1.0f});
139 return result;
140 }
141
142 auto parts = split(accept_header, ',');
143 for (const auto& part : parts) {
144 accept_info info;
145 auto params = split(part, ';');
146
147 if (params.empty()) {
148 continue;
149 }
150
151 info.media_type = trim(params[0]);
152
153 for (size_t i = 1; i < params.size(); ++i) {
154 auto param = trim(params[i]);
155 if (param.starts_with("q=")) {
156 try {
157 info.quality = std::stof(param.substr(2));
158 } catch (...) {
159 info.quality = 1.0f;
160 }
161 } else if (param.starts_with("transfer-syntax=")) {
162 info.transfer_syntax = param.substr(16);
163 // Remove quotes if present
164 if (!info.transfer_syntax.empty() &&
165 info.transfer_syntax.front() == '"') {
166 info.transfer_syntax = info.transfer_syntax.substr(
167 1, info.transfer_syntax.size() - 2);
168 }
169 }
170 }
171
172 result.push_back(std::move(info));
173 }
174
175 // Sort by quality (descending)
176 std::sort(result.begin(), result.end(),
177 [](const accept_info& a, const accept_info& b) {
178 return a.quality > b.quality;
179 });
180
181 return result;
182}

References kcenon::pacs::web::dicomweb::media_type::dicom.

◆ parse_frame_numbers()

auto kcenon::pacs::web::dicomweb::parse_frame_numbers ( std::string_view frame_list) -> std::vector<uint32_t>
nodiscard

Parse frame numbers from URL path.

Parameters
frame_listComma-separated frame numbers (e.g., "1,3,5" or "1-5")
Returns
Vector of frame numbers (1-based), empty on parse error
Examples
  • "1" -> {1}
  • "1,3,5" -> {1, 3, 5}
  • "1-5" -> {1, 2, 3, 4, 5}
  • "1,3-5,7" -> {1, 3, 4, 5, 7}

Definition at line 1121 of file dicomweb_endpoints.cpp.

1121 {
1122 std::vector<uint32_t> frames;
1123
1124 if (frame_list.empty()) {
1125 return frames;
1126 }
1127
1128 // Split by comma
1129 auto parts = split(frame_list, ',');
1130 for (const auto& part : parts) {
1131 auto trimmed = trim(part);
1132 if (trimmed.empty()) {
1133 continue;
1134 }
1135
1136 // Check for range (e.g., "1-5")
1137 auto dash_pos = trimmed.find('-');
1138 if (dash_pos != std::string::npos && dash_pos > 0 &&
1139 dash_pos < trimmed.size() - 1) {
1140 try {
1141 uint32_t start = std::stoul(trimmed.substr(0, dash_pos));
1142 uint32_t end = std::stoul(trimmed.substr(dash_pos + 1));
1143 if (start > 0 && end >= start) {
1144 for (uint32_t i = start; i <= end; ++i) {
1145 frames.push_back(i);
1146 }
1147 }
1148 } catch (...) {
1149 // Invalid range, skip
1150 }
1151 } else {
1152 // Single frame number
1153 try {
1154 uint32_t frame = std::stoul(trimmed);
1155 if (frame > 0) {
1156 frames.push_back(frame);
1157 }
1158 } catch (...) {
1159 // Invalid number, skip
1160 }
1161 }
1162 }
1163
1164 // Remove duplicates while preserving order
1165 std::vector<uint32_t> unique_frames;
1166 for (auto f : frames) {
1167 if (std::find(unique_frames.begin(), unique_frames.end(), f) ==
1168 unique_frames.end()) {
1169 unique_frames.push_back(f);
1170 }
1171 }
1172
1173 return unique_frames;
1174}

◆ parse_instance_query_params()

auto kcenon::pacs::web::dicomweb::parse_instance_query_params ( const std::string & url_params) -> storage::instance_query
nodiscard

Parse QIDO-RS instance query parameters from HTTP request.

Parameters
url_paramsThe URL query parameters
Returns
Instance query parameters

Definition at line 1088 of file dicomweb_endpoints.cpp.

1088 {
1090
1091 auto params = parse_query_string(url_params);
1092 for (const auto& [key, value] : params) {
1093 if (key == "SeriesInstanceUID" || key == "0020000E") {
1094 query.series_uid = value;
1095 } else if (key == "SOPInstanceUID" || key == "00080018") {
1096 query.sop_uid = value;
1097 } else if (key == "SOPClassUID" || key == "00080016") {
1098 query.sop_class_uid = value;
1099 } else if (key == "InstanceNumber" || key == "00200013") {
1100 try {
1101 query.instance_number = std::stoi(value);
1102 } catch (...) {}
1103 } else if (key == "limit") {
1104 try {
1105 query.limit = std::stoull(value);
1106 } catch (...) {}
1107 } else if (key == "offset") {
1108 try {
1109 query.offset = std::stoull(value);
1110 } catch (...) {}
1111 }
1112 }
1113
1114 return query;
1115}
const atna_coded_value query
Query (110112)

◆ parse_rendered_params()

auto kcenon::pacs::web::dicomweb::parse_rendered_params ( std::string_view query_string,
std::string_view accept_header ) -> rendered_params
nodiscard

Parse rendered image parameters from HTTP request.

Parameters
query_stringThe URL query string
accept_headerThe Accept header value
Returns
Parsed rendered parameters

Definition at line 1201 of file dicomweb_endpoints.cpp.

1203 {
1204
1205 rendered_params params;
1206
1207 // Determine format from Accept header
1208 if (accept_header.find("image/jphc") != std::string_view::npos) {
1209 params.format = rendered_format::jphc;
1210 } else if (accept_header.find("image/png") != std::string_view::npos) {
1211 params.format = rendered_format::png;
1212 } else {
1213 params.format = rendered_format::jpeg; // Default to JPEG
1214 }
1215
1216 // Parse query parameters
1217 auto query_params = parse_query_string(std::string(query_string));
1218 for (const auto& [key, value] : query_params) {
1219 if (key == "quality") {
1220 try {
1221 params.quality = std::stoi(value);
1222 params.quality = std::clamp(params.quality, 1, 100);
1223 } catch (...) {}
1224 } else if (key == "windowcenter" || key == "window-center") {
1225 try {
1226 params.window_center = std::stod(value);
1227 } catch (...) {}
1228 } else if (key == "windowwidth" || key == "window-width") {
1229 try {
1230 params.window_width = std::stod(value);
1231 } catch (...) {}
1232 } else if (key == "viewport") {
1233 // Format: WxH or W,H
1234 auto sep = value.find_first_of("x,");
1235 if (sep != std::string::npos) {
1236 try {
1237 params.viewport_width =
1238 static_cast<uint16_t>(std::stoul(value.substr(0, sep)));
1239 params.viewport_height =
1240 static_cast<uint16_t>(std::stoul(value.substr(sep + 1)));
1241 } catch (...) {}
1242 }
1243 } else if (key == "rows") {
1244 try {
1245 params.viewport_height = static_cast<uint16_t>(std::stoul(value));
1246 } catch (...) {}
1247 } else if (key == "columns") {
1248 try {
1249 params.viewport_width = static_cast<uint16_t>(std::stoul(value));
1250 } catch (...) {}
1251 } else if (key == "frame") {
1252 try {
1253 params.frame = std::stoul(value);
1254 if (params.frame == 0) params.frame = 1;
1255 } catch (...) {}
1256 } else if (key == "annotation") {
1257 params.burn_annotations = (value == "true" || value == "1");
1258 }
1259 }
1260
1261 return params;
1262}
Parameters for rendered image requests.
uint16_t viewport_width
Output viewport width (0 = original size)
std::optional< double > window_center
Window center (default: auto from DICOM or calculated)
std::optional< double > window_width
Window width (default: auto from DICOM or calculated)
uint32_t frame
Frame number for multi-frame images (1-based, default 1)
rendered_format format
Output format (jpeg or png)
int quality
JPEG quality (1-100, default 75)
bool burn_annotations
Annotation (burned-in or removed)
uint16_t viewport_height
Output viewport height (0 = original size)

References kcenon::pacs::web::dicomweb::rendered_params::burn_annotations, kcenon::pacs::web::dicomweb::rendered_params::format, kcenon::pacs::web::dicomweb::rendered_params::frame, jpeg, jphc, png, kcenon::pacs::web::dicomweb::rendered_params::quality, kcenon::pacs::web::dicomweb::rendered_params::viewport_height, kcenon::pacs::web::dicomweb::rendered_params::viewport_width, kcenon::pacs::web::dicomweb::rendered_params::window_center, and kcenon::pacs::web::dicomweb::rendered_params::window_width.

◆ parse_series_query_params()

auto kcenon::pacs::web::dicomweb::parse_series_query_params ( const std::string & url_params) -> storage::series_query
nodiscard

Parse QIDO-RS series query parameters from HTTP request.

Parameters
url_paramsThe URL query parameters
Returns
Series query parameters

Definition at line 1055 of file dicomweb_endpoints.cpp.

1055 {
1057
1058 auto params = parse_query_string(url_params);
1059 for (const auto& [key, value] : params) {
1060 if (key == "StudyInstanceUID" || key == "0020000D") {
1061 query.study_uid = value;
1062 } else if (key == "SeriesInstanceUID" || key == "0020000E") {
1063 query.series_uid = value;
1064 } else if (key == "Modality" || key == "00080060") {
1065 query.modality = value;
1066 } else if (key == "SeriesNumber" || key == "00200011") {
1067 try {
1068 query.series_number = std::stoi(value);
1069 } catch (...) {}
1070 } else if (key == "SeriesDescription" || key == "0008103E") {
1071 query.series_description = value;
1072 } else if (key == "BodyPartExamined" || key == "00180015") {
1073 query.body_part_examined = value;
1074 } else if (key == "limit") {
1075 try {
1076 query.limit = std::stoull(value);
1077 } catch (...) {}
1078 } else if (key == "offset") {
1079 try {
1080 query.offset = std::stoull(value);
1081 } catch (...) {}
1082 }
1083 }
1084
1085 return query;
1086}

◆ parse_study_query_params()

auto kcenon::pacs::web::dicomweb::parse_study_query_params ( const std::string & url_params) -> storage::study_query
nodiscard

Parse QIDO-RS query parameters from HTTP request.

Parameters
url_paramsThe URL query parameters
Returns
Study query parameters

Definition at line 1004 of file dicomweb_endpoints.cpp.

1004 {
1006
1007 auto params = parse_query_string(url_params);
1008 for (const auto& [key, value] : params) {
1009 if (key == "PatientID" || key == "00100020") {
1010 query.patient_id = value;
1011 } else if (key == "PatientName" || key == "00100010") {
1012 query.patient_name = value;
1013 } else if (key == "StudyInstanceUID" || key == "0020000D") {
1014 query.study_uid = value;
1015 } else if (key == "StudyID" || key == "00200010") {
1016 query.study_id = value;
1017 } else if (key == "StudyDate" || key == "00080020") {
1018 // Handle date range (YYYYMMDD-YYYYMMDD)
1019 auto dash_pos = value.find('-');
1020 if (dash_pos != std::string::npos && dash_pos > 0 &&
1021 dash_pos < value.size() - 1) {
1022 query.study_date_from = value.substr(0, dash_pos);
1023 query.study_date_to = value.substr(dash_pos + 1);
1024 } else if (dash_pos == 0) {
1025 // -YYYYMMDD (up to date)
1026 query.study_date_to = value.substr(1);
1027 } else if (dash_pos == value.size() - 1) {
1028 // YYYYMMDD- (from date)
1029 query.study_date_from = value.substr(0, dash_pos);
1030 } else {
1031 query.study_date = value;
1032 }
1033 } else if (key == "AccessionNumber" || key == "00080050") {
1034 query.accession_number = value;
1035 } else if (key == "ModalitiesInStudy" || key == "00080061") {
1036 query.modality = value;
1037 } else if (key == "ReferringPhysicianName" || key == "00080090") {
1038 query.referring_physician = value;
1039 } else if (key == "StudyDescription" || key == "00081030") {
1040 query.study_description = value;
1041 } else if (key == "limit") {
1042 try {
1043 query.limit = std::stoull(value);
1044 } catch (...) {}
1045 } else if (key == "offset") {
1046 try {
1047 query.offset = std::stoull(value);
1048 } catch (...) {}
1049 }
1050 }
1051
1052 return query;
1053}

◆ render_dicom_image()

auto kcenon::pacs::web::dicomweb::render_dicom_image ( std::string_view file_path,
const rendered_params & params ) -> rendered_result
nodiscard

Render a DICOM image to JPEG or PNG.

Parameters
file_pathPath to DICOM file
paramsRendering parameters
Returns
Rendered image result

Definition at line 1332 of file dicomweb_endpoints.cpp.

1334 {
1335
1336 // Load DICOM file
1337 std::ifstream file(std::string(file_path), std::ios::binary);
1338 if (!file) {
1339 return rendered_result::error("Failed to open DICOM file");
1340 }
1341
1342 std::vector<uint8_t> file_data(
1343 (std::istreambuf_iterator<char>(file)),
1344 std::istreambuf_iterator<char>());
1345 file.close();
1346
1347 auto dicom_result = core::dicom_file::from_bytes(
1348 std::span<const uint8_t>(file_data.data(), file_data.size()));
1349
1350 if (dicom_result.is_err()) {
1351 return rendered_result::error("Failed to parse DICOM file");
1352 }
1353
1354 const auto& dataset = dicom_result.value().dataset();
1355
1356 // Get image parameters
1357 auto rows_elem = dataset.get(core::tags::rows);
1358 auto cols_elem = dataset.get(core::tags::columns);
1359 auto bits_stored_elem = dataset.get(core::tags::bits_stored);
1360 auto bits_allocated_elem = dataset.get(core::tags::bits_allocated);
1361 auto pixel_rep_elem = dataset.get(core::tags::pixel_representation);
1362 auto samples_elem = dataset.get(core::tags::samples_per_pixel);
1363 auto pixel_data_elem = dataset.get(core::tags::pixel_data);
1364
1365 if (!rows_elem || !cols_elem || !pixel_data_elem) {
1366 return rendered_result::error("Missing required image attributes");
1367 }
1368
1369 uint16_t rows = rows_elem->as_numeric<uint16_t>().unwrap_or(0);
1370 uint16_t cols = cols_elem->as_numeric<uint16_t>().unwrap_or(0);
1371 uint16_t bits_stored = bits_stored_elem ?
1372 bits_stored_elem->as_numeric<uint16_t>().unwrap_or(8) : 8;
1373 uint16_t bits_allocated = bits_allocated_elem ?
1374 bits_allocated_elem->as_numeric<uint16_t>().unwrap_or(8) : 8;
1375 uint16_t pixel_rep = pixel_rep_elem ?
1376 pixel_rep_elem->as_numeric<uint16_t>().unwrap_or(0) : 0;
1377 uint16_t samples_per_pixel = samples_elem ?
1378 samples_elem->as_numeric<uint16_t>().unwrap_or(1) : 1;
1379
1380 bool is_signed = (pixel_rep == 1);
1381
1382 // Get window/level from DICOM or use provided parameters
1383 double window_center = 128.0;
1384 double window_width = 256.0;
1385
1386 if (params.window_center.has_value()) {
1387 window_center = *params.window_center;
1388 } else if (auto wc = dataset.get(core::tags::window_center)) {
1389 try {
1390 auto values_result = wc->as_string_list();
1391 if (values_result.is_ok() && !values_result.value().empty()) {
1392 window_center = std::stod(values_result.value()[0]);
1393 }
1394 } catch (...) {}
1395 }
1396
1397 if (params.window_width.has_value()) {
1398 window_width = *params.window_width;
1399 } else if (auto ww = dataset.get(core::tags::window_width)) {
1400 try {
1401 auto values_result = ww->as_string_list();
1402 if (values_result.is_ok() && !values_result.value().empty()) {
1403 window_width = std::stod(values_result.value()[0]);
1404 }
1405 } catch (...) {}
1406 }
1407
1408 // Get rescale parameters
1409 double rescale_slope = 1.0;
1410 double rescale_intercept = 0.0;
1411
1412 if (auto rs = dataset.get(core::tags::rescale_slope)) {
1413 try {
1414 rescale_slope = std::stod(rs->as_string().unwrap_or("1.0"));
1415 } catch (...) {}
1416 }
1417 if (auto ri = dataset.get(core::tags::rescale_intercept)) {
1418 try {
1419 rescale_intercept = std::stod(ri->as_string().unwrap_or("0.0"));
1420 } catch (...) {}
1421 }
1422
1423 // Get pixel data
1424 auto pixel_data = pixel_data_elem->raw_data();
1425
1426 // Calculate frame size for multi-frame images
1427 size_t frame_size = static_cast<size_t>(rows) * cols * samples_per_pixel *
1428 ((bits_allocated + 7) / 8);
1429
1430 // Extract specific frame if multi-frame
1431 std::span<const uint8_t> frame_data = pixel_data;
1432 if (params.frame > 1) {
1433 auto extracted = extract_frame(pixel_data, params.frame, frame_size);
1434 if (extracted.empty()) {
1435 return rendered_result::error("Requested frame does not exist");
1436 }
1437 // For simplicity, we'll just use first frame for now
1438 // Full implementation would handle multi-frame properly
1439 }
1440
1441 // Apply window/level for grayscale images
1442 std::vector<uint8_t> output_pixels;
1443 if (samples_per_pixel == 1) {
1444 output_pixels = apply_window_level(
1445 frame_data, cols, rows, bits_stored, is_signed,
1446 window_center, window_width, rescale_slope, rescale_intercept);
1447 } else {
1448 // For color images, just use raw data (convert to 8-bit if needed)
1449 output_pixels.resize(static_cast<size_t>(rows) * cols * samples_per_pixel);
1450 if (bits_allocated == 8) {
1451 std::copy(frame_data.begin(),
1452 frame_data.begin() + (std::min)(frame_data.size(),
1453 output_pixels.size()),
1454 output_pixels.begin());
1455 } else {
1456 // Convert 16-bit to 8-bit
1457 for (size_t i = 0; i < output_pixels.size() && i * 2 + 1 < frame_data.size(); ++i) {
1458 uint16_t val = static_cast<uint16_t>(
1459 frame_data[i * 2] | (frame_data[i * 2 + 1] << 8));
1460 output_pixels[i] = static_cast<uint8_t>(val >> (bits_stored - 8));
1461 }
1462 }
1463 }
1464
1465 // Encode to requested format
1466 encoding::compression::image_params img_params;
1467 img_params.width = cols;
1468 img_params.height = rows;
1469 img_params.bits_allocated = 8;
1470 img_params.bits_stored = 8;
1471 img_params.high_bit = 7;
1472 img_params.samples_per_pixel = samples_per_pixel;
1473 img_params.photometric =
1474 (samples_per_pixel == 1) ?
1475 encoding::compression::photometric_interpretation::monochrome2 :
1476 encoding::compression::photometric_interpretation::rgb;
1477
1478 if (params.format == rendered_format::jphc) {
1479 encoding::compression::htj2k_codec codec(
1480 /*lossless=*/false, /*use_rpcl=*/false);
1481
1482 auto result = codec.encode(output_pixels, img_params);
1483 if (result.is_err()) {
1484 return rendered_result::error("HTJ2K encoding failed: " +
1485 result.error().message);
1486 }
1487
1488 return rendered_result::ok(std::move(result.value().data), media_type::jphc);
1489 } else if (params.format == rendered_format::jpeg) {
1490 encoding::compression::jpeg_baseline_codec codec;
1491
1492 encoding::compression::compression_options opts;
1493 opts.quality = params.quality;
1494
1495 auto result = codec.encode(output_pixels, img_params, opts);
1496 if (result.is_err()) {
1497 return rendered_result::error("JPEG encoding failed: " +
1498 result.error().message);
1499 }
1500
1501 return rendered_result::ok(std::move(result.value().data), media_type::jpeg);
1502 } else {
1503 // PNG encoding - not implemented yet
1504 return rendered_result::error("PNG encoding not yet implemented");
1505 }
1506}
constexpr dicom_tag rows
Rows.
constexpr dicom_tag rescale_intercept
Rescale Intercept.
constexpr dicom_tag bits_stored
Bits Stored.
constexpr dicom_tag pixel_data
Pixel Data.
constexpr dicom_tag rescale_slope
Rescale Slope.
constexpr dicom_tag samples_per_pixel
Samples per Pixel.

References apply_window_level(), kcenon::pacs::core::tags::bits_allocated, kcenon::pacs::encoding::compression::image_params::bits_allocated, kcenon::pacs::core::tags::bits_stored, kcenon::pacs::encoding::compression::image_params::bits_stored, kcenon::pacs::core::tags::columns, kcenon::pacs::encoding::compression::htj2k_codec::encode(), kcenon::pacs::encoding::compression::jpeg_baseline_codec::encode(), kcenon::pacs::web::dicomweb::rendered_result::error(), extract_frame(), kcenon::pacs::core::dicom_file::from_bytes(), kcenon::pacs::encoding::compression::image_params::height, kcenon::pacs::encoding::compression::image_params::high_bit, kcenon::pacs::web::dicomweb::media_type::jpeg, jpeg, kcenon::pacs::web::dicomweb::media_type::jphc, jphc, kcenon::pacs::encoding::compression::monochrome2, kcenon::pacs::web::dicomweb::rendered_result::ok(), kcenon::pacs::encoding::compression::image_params::photometric, kcenon::pacs::core::tags::pixel_data, kcenon::pacs::core::tags::pixel_representation, kcenon::pacs::encoding::compression::compression_options::quality, kcenon::pacs::core::tags::rescale_intercept, kcenon::pacs::core::tags::rescale_slope, kcenon::pacs::encoding::compression::rgb, kcenon::pacs::core::tags::rows, kcenon::pacs::core::tags::samples_per_pixel, kcenon::pacs::encoding::compression::image_params::samples_per_pixel, kcenon::pacs::encoding::compression::image_params::width, kcenon::pacs::core::tags::window_center, and kcenon::pacs::core::tags::window_width.

Here is the call graph for this function:

◆ series_record_to_dicom_json()

auto kcenon::pacs::web::dicomweb::series_record_to_dicom_json ( const storage::series_record & record,
std::string_view study_uid ) -> std::string
nodiscard

Convert a series record to DicomJSON format for QIDO-RS response.

Parameters
recordThe series record to convert
study_uidThe Study Instance UID for this series
Returns
DicomJSON string for the series

Definition at line 835 of file dicomweb_endpoints.cpp.

837 {
838 std::ostringstream oss;
839 oss << "{";
840
841 // Series Instance UID (0020,000E)
842 oss << "\"0020000E\":{\"vr\":\"UI\",\"Value\":[\""
843 << json_escape(record.series_uid) << "\"]}";
844
845 // Study Instance UID (0020,000D)
846 if (!study_uid.empty()) {
847 oss << ",\"0020000D\":{\"vr\":\"UI\",\"Value\":[\""
848 << json_escape(std::string(study_uid)) << "\"]}";
849 }
850
851 // Modality (0008,0060)
852 if (!record.modality.empty()) {
853 oss << ",\"00080060\":{\"vr\":\"CS\",\"Value\":[\""
854 << json_escape(record.modality) << "\"]}";
855 }
856
857 // Series Number (0020,0011)
858 if (record.series_number.has_value()) {
859 oss << ",\"00200011\":{\"vr\":\"IS\",\"Value\":["
860 << *record.series_number << "]}";
861 }
862
863 // Series Description (0008,103E)
864 if (!record.series_description.empty()) {
865 oss << ",\"0008103E\":{\"vr\":\"LO\",\"Value\":[\""
866 << json_escape(record.series_description) << "\"]}";
867 }
868
869 // Body Part Examined (0018,0015)
870 if (!record.body_part_examined.empty()) {
871 oss << ",\"00180015\":{\"vr\":\"CS\",\"Value\":[\""
872 << json_escape(record.body_part_examined) << "\"]}";
873 }
874
875 // Number of Series Related Instances (0020,1209)
876 oss << ",\"00201209\":{\"vr\":\"IS\",\"Value\":[" << record.num_instances << "]}";
877
878 oss << "}";
879 return oss.str();
880}

References kcenon::pacs::web::json_escape().

Here is the call graph for this function:

◆ study_record_to_dicom_json()

auto kcenon::pacs::web::dicomweb::study_record_to_dicom_json ( const storage::study_record & record,
std::string_view patient_id,
std::string_view patient_name ) -> std::string
nodiscard

Convert a study record to DicomJSON format for QIDO-RS response.

Parameters
recordThe study record to convert
patient_idThe patient ID for this study
patient_nameThe patient name for this study
Returns
DicomJSON string for the study

Definition at line 753 of file dicomweb_endpoints.cpp.

756 {
757 std::ostringstream oss;
758 oss << "{";
759
760 // Study Instance UID (0020,000D)
761 oss << "\"0020000D\":{\"vr\":\"UI\",\"Value\":[\""
762 << json_escape(record.study_uid) << "\"]}";
763
764 // Study Date (0008,0020)
765 if (!record.study_date.empty()) {
766 oss << ",\"00080020\":{\"vr\":\"DA\",\"Value\":[\""
767 << json_escape(record.study_date) << "\"]}";
768 }
769
770 // Study Time (0008,0030)
771 if (!record.study_time.empty()) {
772 oss << ",\"00080030\":{\"vr\":\"TM\",\"Value\":[\""
773 << json_escape(record.study_time) << "\"]}";
774 }
775
776 // Accession Number (0008,0050)
777 if (!record.accession_number.empty()) {
778 oss << ",\"00080050\":{\"vr\":\"SH\",\"Value\":[\""
779 << json_escape(record.accession_number) << "\"]}";
780 }
781
782 // Modalities in Study (0008,0061)
783 if (!record.modalities_in_study.empty()) {
784 oss << ",\"00080061\":{\"vr\":\"CS\",\"Value\":[";
785 auto modalities = split(record.modalities_in_study, '\\');
786 bool first = true;
787 for (const auto& mod : modalities) {
788 if (!first) oss << ",";
789 first = false;
790 oss << "\"" << json_escape(mod) << "\"";
791 }
792 oss << "]}";
793 }
794
795 // Referring Physician's Name (0008,0090)
796 if (!record.referring_physician.empty()) {
797 oss << ",\"00080090\":{\"vr\":\"PN\",\"Value\":[{\"Alphabetic\":\""
798 << json_escape(record.referring_physician) << "\"}]}";
799 }
800
801 // Patient's Name (0010,0010)
802 if (!patient_name.empty()) {
803 oss << ",\"00100010\":{\"vr\":\"PN\",\"Value\":[{\"Alphabetic\":\""
804 << json_escape(std::string(patient_name)) << "\"}]}";
805 }
806
807 // Patient ID (0010,0020)
808 if (!patient_id.empty()) {
809 oss << ",\"00100020\":{\"vr\":\"LO\",\"Value\":[\""
810 << json_escape(std::string(patient_id)) << "\"]}";
811 }
812
813 // Study ID (0020,0010)
814 if (!record.study_id.empty()) {
815 oss << ",\"00200010\":{\"vr\":\"SH\",\"Value\":[\""
816 << json_escape(record.study_id) << "\"]}";
817 }
818
819 // Study Description (0008,1030)
820 if (!record.study_description.empty()) {
821 oss << ",\"00081030\":{\"vr\":\"LO\",\"Value\":[\""
822 << json_escape(record.study_description) << "\"]}";
823 }
824
825 // Number of Study Related Series (0020,1206)
826 oss << ",\"00201206\":{\"vr\":\"IS\",\"Value\":[" << record.num_series << "]}";
827
828 // Number of Study Related Instances (0020,1208)
829 oss << ",\"00201208\":{\"vr\":\"IS\",\"Value\":[" << record.num_instances << "]}";
830
831 oss << "}";
832 return oss.str();
833}
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag patient_name
Patient's Name.

References kcenon::pacs::web::json_escape().

Here is the call graph for this function:

◆ validate_instance()

auto kcenon::pacs::web::dicomweb::validate_instance ( const core::dicom_dataset & dataset,
std::optional< std::string_view > target_study_uid = std::nullopt ) -> validation_result
nodiscard

Validate a DICOM instance for STOW-RS storage.

Parameters
datasetThe DICOM dataset to validate
target_study_uidOptional target study UID (for existing study)
Returns
Validation result

Definition at line 551 of file dicomweb_endpoints.cpp.

553 {
554
555 // Check required DICOM tags
556 auto sop_class = dataset.get(core::tags::sop_class_uid);
557 if (!sop_class || sop_class->as_string().unwrap_or("").empty()) {
558 return validation_result::error(
559 "MISSING_SOP_CLASS",
560 "SOP Class UID (0008,0016) is required");
561 }
562
563 auto sop_instance = dataset.get(core::tags::sop_instance_uid);
564 if (!sop_instance || sop_instance->as_string().unwrap_or("").empty()) {
565 return validation_result::error(
566 "MISSING_SOP_INSTANCE",
567 "SOP Instance UID (0008,0018) is required");
568 }
569
570 auto study_uid = dataset.get(core::tags::study_instance_uid);
571 if (!study_uid || study_uid->as_string().unwrap_or("").empty()) {
572 return validation_result::error(
573 "MISSING_STUDY_UID",
574 "Study Instance UID (0020,000D) is required");
575 }
576
577 auto series_uid = dataset.get(core::tags::series_instance_uid);
578 if (!series_uid || series_uid->as_string().unwrap_or("").empty()) {
579 return validation_result::error(
580 "MISSING_SERIES_UID",
581 "Series Instance UID (0020,000E) is required");
582 }
583
584 // Validate study UID matches target if specified
585 if (target_study_uid.has_value()) {
586 auto instance_study_uid = study_uid->as_string().unwrap_or("");
587 if (instance_study_uid != *target_study_uid) {
588 return validation_result::error(
589 "STUDY_UID_MISMATCH",
590 "Instance Study UID does not match target study");
591 }
592 }
593
594 return validation_result::ok();
595}
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.

References kcenon::pacs::web::dicomweb::validation_result::error(), kcenon::pacs::web::dicomweb::validation_result::ok(), kcenon::pacs::core::tags::series_instance_uid, kcenon::pacs::core::tags::sop_class_uid, kcenon::pacs::core::tags::sop_instance_uid, and kcenon::pacs::core::tags::study_instance_uid.

Here is the call graph for this function:

◆ vr_to_string()

auto kcenon::pacs::web::dicomweb::vr_to_string ( uint16_t vr_code) -> std::string
nodiscard

Convert a VR type code to DicomJSON VR string.

Parameters
vr_codeThe VR type code
Returns
VR string (e.g., "PN", "LO", "UI")

Definition at line 320 of file dicomweb_endpoints.cpp.

320 {
321 // Convert 2-byte VR code to string
322 char vr[3] = {static_cast<char>(vr_code & 0xFF),
323 static_cast<char>((vr_code >> 8) & 0xFF),
324 '\0'};
325 return std::string(vr);
326}
vr_encoding vr

References vr.