PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
explicit_vr_codec.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
11
14
15#include <cstring>
16
17namespace kcenon::pacs::encoding {
18
19namespace {
20
21// ============================================================================
22// Byte Order Utilities (Little Endian)
23// ============================================================================
24
25constexpr uint16_t read_le16(const uint8_t* data) {
26 return static_cast<uint16_t>(data[0]) |
27 (static_cast<uint16_t>(data[1]) << 8);
28}
29
30constexpr uint32_t read_le32(const uint8_t* data) {
31 return static_cast<uint32_t>(data[0]) |
32 (static_cast<uint32_t>(data[1]) << 8) |
33 (static_cast<uint32_t>(data[2]) << 16) |
34 (static_cast<uint32_t>(data[3]) << 24);
35}
36
37void write_le16(std::vector<uint8_t>& buffer, uint16_t value) {
38 buffer.push_back(static_cast<uint8_t>(value & 0xFF));
39 buffer.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
40}
41
42void write_le32(std::vector<uint8_t>& buffer, uint32_t value) {
43 buffer.push_back(static_cast<uint8_t>(value & 0xFF));
44 buffer.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
45 buffer.push_back(static_cast<uint8_t>((value >> 16) & 0xFF));
46 buffer.push_back(static_cast<uint8_t>((value >> 24) & 0xFF));
47}
48
49// ============================================================================
50// DICOM Special Tags
51// ============================================================================
52
53constexpr uint16_t ITEM_GROUP = 0xFFFE;
54constexpr uint16_t ITEM_TAG_ELEMENT = 0xE000; // Item
55constexpr uint16_t ITEM_DELIM_ELEMENT = 0xE00D; // Item Delimitation Item
56constexpr uint16_t SEQ_DELIM_ELEMENT = 0xE0DD; // Sequence Delimitation Item
57
58constexpr uint32_t UNDEFINED_LENGTH = 0xFFFFFFFF;
59
60constexpr bool is_sequence_delimiter(core::dicom_tag tag) {
61 return tag.group() == ITEM_GROUP && tag.element() == SEQ_DELIM_ELEMENT;
62}
63
64constexpr bool is_item_delimiter(core::dicom_tag tag) {
65 return tag.group() == ITEM_GROUP && tag.element() == ITEM_DELIM_ELEMENT;
66}
67
68constexpr bool is_item_tag(core::dicom_tag tag) {
69 return tag.group() == ITEM_GROUP && tag.element() == ITEM_TAG_ELEMENT;
70}
71
72constexpr bool is_special_tag(core::dicom_tag tag) {
73 return tag.group() == ITEM_GROUP;
74}
75
76} // namespace
77
78// ============================================================================
79// Error Helpers
80// ============================================================================
81
82namespace {
83
84template <typename T>
85kcenon::pacs::Result<T> make_codec_error(int code, const std::string& message) {
86 return kcenon::pacs::pacs_error<T>(code, message);
87}
88
89} // namespace
90
91// ============================================================================
92// Dataset Encoding
93// ============================================================================
94
95std::vector<uint8_t> explicit_vr_codec::encode(
96 const core::dicom_dataset& dataset) {
97 std::vector<uint8_t> buffer;
98 buffer.reserve(4096); // Initial capacity
99
100 for (const auto& [tag, element] : dataset) {
101 auto encoded = encode_element(element);
102 buffer.insert(buffer.end(), encoded.begin(), encoded.end());
103 }
104
105 return buffer;
106}
107
109 const core::dicom_element& element) {
110 std::vector<uint8_t> buffer;
111
112 // Write tag (4 bytes: group + element, little-endian)
113 write_le16(buffer, element.tag().group());
114 write_le16(buffer, element.tag().element());
115
116 // Write VR (2 ASCII characters)
117 auto vr_str = to_string(element.vr());
118 buffer.push_back(static_cast<uint8_t>(vr_str[0]));
119 buffer.push_back(static_cast<uint8_t>(vr_str[1]));
120
121 // Handle sequences specially
122 if (element.is_sequence()) {
123 encode_sequence(buffer, element);
124 return buffer;
125 }
126
127 // Get padded data
128 auto data = element.raw_data();
129 auto padded_data = pad_to_even(element.vr(), data);
130 uint32_t length = static_cast<uint32_t>(padded_data.size());
131
132 // Write length based on VR type
133 if (has_explicit_32bit_length(element.vr())) {
134 // Extended format: 2 reserved bytes + 4 byte length
135 write_le16(buffer, 0x0000); // Reserved
136 write_le32(buffer, length);
137 } else {
138 // Standard format: 2 byte length
139 write_le16(buffer, static_cast<uint16_t>(length));
140 }
141
142 // Write value
143 buffer.insert(buffer.end(), padded_data.begin(), padded_data.end());
144
145 return buffer;
146}
147
148void explicit_vr_codec::encode_sequence(std::vector<uint8_t>& buffer,
149 const core::dicom_element& element) {
150 // Write reserved bytes for SQ VR (uses 32-bit length)
151 write_le16(buffer, 0x0000);
152
153 // Write undefined length for sequence
154 write_le32(buffer, UNDEFINED_LENGTH);
155
156 // Encode each sequence item
157 const auto& items = element.sequence_items();
158 for (const auto& item : items) {
159 encode_sequence_item(buffer, item);
160 }
161
162 // Write sequence delimitation item (implicit VR encoding for delimiters)
163 write_le16(buffer, ITEM_GROUP);
164 write_le16(buffer, SEQ_DELIM_ELEMENT);
165 write_le32(buffer, 0); // Length is always 0 for delimiter
166}
167
168void explicit_vr_codec::encode_sequence_item(std::vector<uint8_t>& buffer,
169 const core::dicom_dataset& item) {
170 // Write Item tag (implicit VR encoding for item tags)
171 write_le16(buffer, ITEM_GROUP);
172 write_le16(buffer, ITEM_TAG_ELEMENT);
173
174 // Encode item content first to determine length
175 auto item_content = encode(item);
176
177 // Write item length
178 write_le32(buffer, static_cast<uint32_t>(item_content.size()));
179
180 // Write item content
181 buffer.insert(buffer.end(), item_content.begin(), item_content.end());
182}
183
184// ============================================================================
185// Dataset Decoding
186// ============================================================================
187
189 std::span<const uint8_t> data) {
190 core::dicom_dataset dataset;
191
192 while (!data.empty()) {
193 // Peek at tag to check for sequence delimiters
194 if (data.size() >= 4) {
195 uint16_t group = read_le16(data.data());
196 uint16_t elem = read_le16(data.data() + 2);
197 core::dicom_tag tag{group, elem};
198
199 // Stop at sequence delimiter
200 if (is_sequence_delimiter(tag)) {
201 break;
202 }
203 // Stop at item delimiter
204 if (is_item_delimiter(tag)) {
205 break;
206 }
207 }
208
209 auto result = decode_element(data);
210 if (!result.is_ok()) {
212 result.error().code,
213 result.error().message);
214 }
215
216 dataset.insert(std::move(result.value()));
217 }
218
219 return dataset;
220}
221
223 std::span<const uint8_t>& data) {
224 // Need at least 8 bytes for standard format: tag (4) + VR (2) + length (2)
225 if (data.size() < 8) {
226 return make_codec_error<core::dicom_element>(
228 "Insufficient data to decode element");
229 }
230
231 // Read tag
232 uint16_t group = read_le16(data.data());
233 uint16_t elem = read_le16(data.data() + 2);
234 core::dicom_tag tag{group, elem};
235
236 // Special handling for sequence delimiter/item tags (implicit VR format)
237 if (is_special_tag(tag)) {
238 data = data.subspan(8);
239
240 // For delimiter tags, return a placeholder element
241 return core::dicom_element(tag, vr_type::UN);
242 }
243
244 // Read VR (2 ASCII characters)
245 char vr_chars[2];
246 vr_chars[0] = static_cast<char>(data[4]);
247 vr_chars[1] = static_cast<char>(data[5]);
248 std::string_view vr_str(vr_chars, 2);
249
250 auto vr_opt = from_string(vr_str);
251 if (!vr_opt) {
252 return make_codec_error<core::dicom_element>(
254 "Unknown VR type");
255 }
256 vr_type vr = *vr_opt;
257
258 uint32_t length;
259 size_t header_size;
260
261 // Determine length format based on VR
263 // Extended format: need 12 bytes total
264 if (data.size() < 12) {
265 return make_codec_error<core::dicom_element>(
267 "Insufficient data for extended VR format");
268 }
269 // Skip reserved 2 bytes, then read 4-byte length
270 length = read_le32(data.data() + 8);
271 header_size = 12;
272 } else {
273 // Standard format: 2-byte length
274 length = read_le16(data.data() + 6);
275 header_size = 8;
276 }
277
278 data = data.subspan(header_size);
279
280 // Handle undefined length (sequences and encapsulated data)
281 if (length == UNDEFINED_LENGTH) {
282 return decode_undefined_length(tag, vr, data);
283 }
284
285 // Check if we have enough data
286 if (data.size() < length) {
287 return make_codec_error<core::dicom_element>(
289 "Insufficient data for element value");
290 }
291
292 // Read value
293 auto value_data = data.subspan(0, length);
294 data = data.subspan(length);
295
296 return core::dicom_element(tag, vr, value_data);
297}
298
301 std::span<const uint8_t>& data) {
302 // If this is a sequence (SQ), decode sequence items
303 if (vr == vr_type::SQ) {
304 core::dicom_element seq_element(tag, vr_type::SQ);
305
306 while (!data.empty()) {
307 // Check for sequence delimitation
308 if (data.size() < 8) {
309 return make_codec_error<core::dicom_element>(
311 "Insufficient data for sequence delimiter");
312 }
313
314 uint16_t item_group = read_le16(data.data());
315 uint16_t item_elem = read_le16(data.data() + 2);
316 core::dicom_tag item_tag{item_group, item_elem};
317
318 // Check for sequence delimitation item
319 if (is_sequence_delimiter(item_tag)) {
320 // Skip the delimiter (8 bytes: tag + length)
321 data = data.subspan(8);
322 break;
323 }
324
325 // Must be an Item tag
326 if (!is_item_tag(item_tag)) {
327 return make_codec_error<core::dicom_element>(
329 "Expected Item tag in sequence");
330 }
331
332 // Decode the sequence item
333 auto item_result = decode_sequence_item(data);
334 if (!item_result.is_ok()) {
335 return make_codec_error<core::dicom_element>(
336 item_result.error().code,
337 item_result.error().message);
338 }
339
340 seq_element.sequence_items().push_back(std::move(item_result.value()));
341 }
342
343 return seq_element;
344 }
345
346 // For other undefined-length elements (like encapsulated pixel data),
347 // read until we find a sequence delimitation item
348 std::vector<uint8_t> accumulated_data;
349
350 while (!data.empty()) {
351 if (data.size() < 8) {
352 return make_codec_error<core::dicom_element>(
354 "Insufficient data for encapsulated data");
355 }
356
357 uint16_t item_group = read_le16(data.data());
358 uint16_t item_elem = read_le16(data.data() + 2);
359 core::dicom_tag item_tag{item_group, item_elem};
360
361 // Check for sequence delimitation item
362 if (is_sequence_delimiter(item_tag)) {
363 data = data.subspan(8);
364 break;
365 }
366
367 // For encapsulated data, read item tag + length + data
368 if (is_item_tag(item_tag)) {
369 uint32_t item_length = read_le32(data.data() + 4);
370 data = data.subspan(8);
371
372 if (item_length != UNDEFINED_LENGTH && data.size() >= item_length) {
373 accumulated_data.insert(accumulated_data.end(),
374 data.begin(), data.begin() + item_length);
375 data = data.subspan(item_length);
376 }
377 } else {
378 return make_codec_error<core::dicom_element>(
380 "Invalid tag in encapsulated data");
381 }
382 }
383
384 return core::dicom_element(tag, vr, accumulated_data);
385}
386
388 std::span<const uint8_t>& data) {
389 // Read Item tag and length (implicit VR format for item tags)
390 if (data.size() < 8) {
391 return make_codec_error<core::dicom_dataset>(
393 "Insufficient data for sequence item");
394 }
395
396 uint16_t group = read_le16(data.data());
397 uint16_t elem = read_le16(data.data() + 2);
398 core::dicom_tag tag{group, elem};
399
400 if (!is_item_tag(tag)) {
401 return make_codec_error<core::dicom_dataset>(
403 "Expected Item tag for sequence item");
404 }
405
406 uint32_t item_length = read_le32(data.data() + 4);
407 data = data.subspan(8);
408
409 if (item_length == UNDEFINED_LENGTH) {
410 // Decode until Item Delimitation Item
412
413 while (!data.empty()) {
414 if (data.size() < 4) {
415 return make_codec_error<core::dicom_dataset>(
417 "Insufficient data for item delimiter check");
418 }
419
420 uint16_t elem_group = read_le16(data.data());
421 uint16_t elem_elem = read_le16(data.data() + 2);
422 core::dicom_tag elem_tag{elem_group, elem_elem};
423
424 if (is_item_delimiter(elem_tag)) {
425 // Skip delimiter
426 data = data.subspan(8);
427 break;
428 }
429
430 auto elem_result = decode_element(data);
431 if (!elem_result.is_ok()) {
432 return make_codec_error<core::dicom_dataset>(
433 elem_result.error().code,
434 elem_result.error().message);
435 }
436
437 item.insert(std::move(elem_result.value()));
438 }
439
440 return item;
441 }
442
443 // Explicit item length
444 if (data.size() < item_length) {
445 return make_codec_error<core::dicom_dataset>(
447 "Insufficient data for item content");
448 }
449
450 auto item_data = data.subspan(0, item_length);
451 data = data.subspan(item_length);
452
453 return decode(item_data);
454}
455
456} // namespace kcenon::pacs::encoding
void insert(dicom_element element)
Insert or replace an element in the dataset.
auto is_sequence() const noexcept -> bool
Check if this element is a sequence.
constexpr auto tag() const noexcept -> dicom_tag
Get the element's tag.
auto raw_data() const noexcept -> std::span< const uint8_t >
Get the raw data bytes.
constexpr auto vr() const noexcept -> encoding::vr_type
Get the element's VR.
auto sequence_items() -> std::vector< dicom_dataset > &
Get mutable access to sequence items.
static result< core::dicom_element > decode_undefined_length(core::dicom_tag tag, vr_type vr, std::span< const uint8_t > &data)
static void encode_sequence_item(std::vector< uint8_t > &buffer, const core::dicom_dataset &item)
static result< core::dicom_element > decode_element(std::span< const uint8_t > &data)
Decode a single element from bytes.
static void encode_sequence(std::vector< uint8_t > &buffer, const core::dicom_element &element)
static result< core::dicom_dataset > decode_sequence_item(std::span< const uint8_t > &data)
static result< core::dicom_dataset > decode(std::span< const uint8_t > data)
Decode bytes to a dataset using Explicit VR Little Endian.
static std::vector< uint8_t > encode_element(const core::dicom_element &element)
Encode a single element to bytes.
static std::vector< uint8_t > encode(const core::dicom_dataset &dataset)
Encode a dataset to bytes using Explicit VR Little Endian.
Encoder/decoder for Explicit VR Little Endian transfer syntax.
vr_type
DICOM Value Representation (VR) types.
Definition vr_type.h:29
@ UN
Unknown (variable length)
@ SQ
Sequence of Items (undefined length)
constexpr std::optional< vr_type > from_string(std::string_view str) noexcept
Parses a two-character string to a vr_type.
Definition vr_type.h:132
std::vector< uint8_t > pad_to_even(vr_type vr, std::span< const uint8_t > data)
Pads data to even length as required by DICOM.
Definition vr_info.cpp:196
constexpr bool has_explicit_32bit_length(vr_type vr) noexcept
Checks if a VR requires 32-bit length field in Explicit VR encoding.
Definition vr_type.h:235
constexpr std::string_view to_string(vr_type vr) noexcept
Converts a vr_type to its two-character string representation.
Definition vr_type.h:83
constexpr int insufficient_data
Definition result.h:82
constexpr int invalid_sequence
Definition result.h:83
constexpr int unknown_vr
Definition result.h:84
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Definition result.h:234
std::string_view code
vr_encoding vr