PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
rle_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
8
9#include <algorithm>
10#include <array>
11#include <cstdint>
12#include <cstring>
13#include <stdexcept>
14#include <vector>
15
17
18namespace {
19
23inline uint32_t read_le32(const uint8_t* data) {
24 return static_cast<uint32_t>(data[0]) |
25 (static_cast<uint32_t>(data[1]) << 8) |
26 (static_cast<uint32_t>(data[2]) << 16) |
27 (static_cast<uint32_t>(data[3]) << 24);
28}
29
33inline void write_le32(uint8_t* data, uint32_t value) {
34 data[0] = static_cast<uint8_t>(value & 0xFF);
35 data[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
36 data[2] = static_cast<uint8_t>((value >> 16) & 0xFF);
37 data[3] = static_cast<uint8_t>((value >> 24) & 0xFF);
38}
39
52void encode_rle_segment(std::span<const uint8_t> input, std::vector<uint8_t>& output) {
53 if (input.empty()) {
54 return;
55 }
56
57 size_t pos = 0;
58 const size_t size = input.size();
59
60 while (pos < size) {
61 // Look for runs of identical bytes
62 uint8_t current = input[pos];
63 size_t run_length = 1;
64
65 while (pos + run_length < size &&
66 input[pos + run_length] == current &&
67 run_length < 128) {
68 ++run_length;
69 }
70
71 if (run_length >= 3) {
72 // Encode as a run (replicate)
73 // Control byte = 1 - run_length (range: -127 to -2 for lengths 3-128)
74 output.push_back(static_cast<uint8_t>(1 - static_cast<int>(run_length)));
75 output.push_back(current);
76 pos += run_length;
77 } else {
78 // Collect literal bytes (non-repeating or short runs)
79 std::vector<uint8_t> literal;
80 literal.reserve(128);
81
82 while (pos < size && literal.size() < 128) {
83 // Check if a run of 3+ starts here
84 size_t ahead_run = 1;
85 while (pos + ahead_run < size &&
86 input[pos + ahead_run] == input[pos] &&
87 ahead_run < 3) {
88 ++ahead_run;
89 }
90
91 if (ahead_run >= 3) {
92 // Don't include this in literal, it's the start of a run
93 break;
94 }
95
96 literal.push_back(input[pos]);
97 ++pos;
98 }
99
100 if (!literal.empty()) {
101 // Control byte = literal.size() - 1 (range: 0 to 127)
102 output.push_back(static_cast<uint8_t>(literal.size() - 1));
103 output.insert(output.end(), literal.begin(), literal.end());
104 }
105 }
106 }
107}
108
117std::vector<uint8_t> decode_rle_segment(std::span<const uint8_t> input, size_t expected_size) {
118 std::vector<uint8_t> output;
119 output.reserve(expected_size);
120
121 size_t pos = 0;
122 const size_t size = input.size();
123
124 while (pos < size && output.size() < expected_size) {
125 auto control = static_cast<int8_t>(input[pos]);
126 ++pos;
127
128 if (control >= 0) {
129 // Literal: copy next (control + 1) bytes
130 size_t count = static_cast<size_t>(control) + 1;
131 if (pos + count > size) {
132 throw std::runtime_error("RLE decode: insufficient literal data");
133 }
134 output.insert(output.end(), input.begin() + pos, input.begin() + pos + count);
135 pos += count;
136 } else if (control != -128) {
137 // Run: repeat next byte (1 - control) times
138 if (pos >= size) {
139 throw std::runtime_error("RLE decode: missing replicate byte");
140 }
141 size_t count = static_cast<size_t>(1 - control);
142 uint8_t value = input[pos];
143 ++pos;
144 for (size_t i = 0; i < count && output.size() < expected_size; ++i) {
145 output.push_back(value);
146 }
147 }
148 // control == -128 is a no-op
149 }
150
151 return output;
152}
153
163int calculate_segment_count(const image_params& params) {
164 int bytes_per_sample = (params.bits_allocated + 7) / 8;
165 return params.samples_per_pixel * bytes_per_sample;
166}
167
168} // namespace
169
174public:
175 impl() = default;
176
177 [[nodiscard]] codec_result encode(
178 std::span<const uint8_t> pixel_data,
179 const image_params& params,
180 [[maybe_unused]] const compression_options& options) const {
181
182 if (pixel_data.empty()) {
184 }
185
186 if (!valid_for_rle(params)) {
188 "Invalid parameters for RLE: requires 8/16-bit, 1-3 samples per pixel");
189 }
190
191 size_t expected_size = params.frame_size_bytes();
192 if (pixel_data.size() != expected_size) {
194 "Pixel data size mismatch: expected " + std::to_string(expected_size) +
195 ", got " + std::to_string(pixel_data.size()));
196 }
197
198 try {
199 return encode_frame(pixel_data, params);
200 } catch (const std::exception& e) {
201 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, std::string("RLE encoding failed: ") + e.what());
202 }
203 }
204
205 [[nodiscard]] codec_result decode(
206 std::span<const uint8_t> compressed_data,
207 const image_params& params) const {
208
209 if (compressed_data.empty()) {
211 }
212
213 if (compressed_data.size() < kRLEHeaderSize) {
215 }
216
217 try {
218 return decode_frame(compressed_data, params);
219 } catch (const std::exception& e) {
220 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, std::string("RLE decoding failed: ") + e.what());
221 }
222 }
223
224private:
225 [[nodiscard]] bool valid_for_rle(const image_params& params) const noexcept {
226 // RLE supports 8-bit and 16-bit samples
227 if (params.bits_allocated != 8 && params.bits_allocated != 16) {
228 return false;
229 }
230 // 1-3 samples per pixel (grayscale, RGB)
231 if (params.samples_per_pixel < 1 || params.samples_per_pixel > 3) {
232 return false;
233 }
234 // Check segment count limit
235 int num_segments = calculate_segment_count(params);
236 if (num_segments > kMaxSegments) {
237 return false;
238 }
239 // Valid dimensions
240 if (params.width == 0 || params.height == 0) {
241 return false;
242 }
243 return true;
244 }
245
247 std::span<const uint8_t> pixel_data,
248 const image_params& params) const {
249
250 int num_segments = calculate_segment_count(params);
251 size_t pixels_per_frame = static_cast<size_t>(params.width) * params.height;
252 int bytes_per_sample = (params.bits_allocated + 7) / 8;
253
254 // Prepare segment data (one vector per segment)
255 std::vector<std::vector<uint8_t>> segments(num_segments);
256 for (auto& seg : segments) {
257 seg.reserve(pixels_per_frame);
258 }
259
260 // Extract segments from pixel data using SIMD optimization
261 // DICOM RLE stores each byte plane separately
262 // For 16-bit: high byte first, then low byte
263 // For color: each color component separately
264
265 if (bytes_per_sample == 1) {
266 // 8-bit samples
267 if (params.samples_per_pixel == 1) {
268 // Grayscale: single segment - direct copy
269 segments[0].assign(pixel_data.begin(), pixel_data.end());
270 } else if (params.samples_per_pixel == 3) {
271 // Color (RGB): 3 segments - use SIMD deinterleaving
272 segments[0].resize(pixels_per_frame);
273 segments[1].resize(pixels_per_frame);
274 segments[2].resize(pixels_per_frame);
276 pixel_data.data(),
277 segments[0].data(),
278 segments[1].data(),
279 segments[2].data(),
280 pixels_per_frame);
281 } else {
282 // Other sample counts: scalar fallback
283 for (size_t i = 0; i < pixels_per_frame; ++i) {
284 for (int s = 0; s < params.samples_per_pixel; ++s) {
285 segments[s].push_back(pixel_data[i * params.samples_per_pixel + s]);
286 }
287 }
288 }
289 } else {
290 // 16-bit samples (little-endian input)
291 // DICOM RLE stores: all high bytes, then all low bytes
292 if (params.samples_per_pixel == 1) {
293 // Grayscale: 2 segments (high byte, low byte) - use SIMD
294 segments[0].resize(pixels_per_frame);
295 segments[1].resize(pixels_per_frame);
297 pixel_data.data(),
298 segments[0].data(), // High byte
299 segments[1].data(), // Low byte
300 pixels_per_frame);
301 } else if (params.samples_per_pixel == 3) {
302 // 16-bit Color: 6 segments - SIMD for each color component
303 // First deinterleave RGB, then split each into high/low
304 std::vector<uint8_t> r_plane(pixels_per_frame * 2);
305 std::vector<uint8_t> g_plane(pixels_per_frame * 2);
306 std::vector<uint8_t> b_plane(pixels_per_frame * 2);
307
308 // Deinterleave 16-bit RGB (scalar for now, 16-bit RGB is rare)
309 for (size_t i = 0; i < pixels_per_frame; ++i) {
310 size_t src_idx = i * 6; // 3 samples * 2 bytes
311 size_t dst_idx = i * 2;
312 r_plane[dst_idx] = pixel_data[src_idx];
313 r_plane[dst_idx + 1] = pixel_data[src_idx + 1];
314 g_plane[dst_idx] = pixel_data[src_idx + 2];
315 g_plane[dst_idx + 1] = pixel_data[src_idx + 3];
316 b_plane[dst_idx] = pixel_data[src_idx + 4];
317 b_plane[dst_idx + 1] = pixel_data[src_idx + 5];
318 }
319
320 // Split each color plane into high/low using SIMD
321 segments[0].resize(pixels_per_frame); // R high
322 segments[1].resize(pixels_per_frame); // R low
323 segments[2].resize(pixels_per_frame); // G high
324 segments[3].resize(pixels_per_frame); // G low
325 segments[4].resize(pixels_per_frame); // B high
326 segments[5].resize(pixels_per_frame); // B low
327
328 simd::split_16bit_to_planes(r_plane.data(), segments[0].data(),
329 segments[1].data(), pixels_per_frame);
330 simd::split_16bit_to_planes(g_plane.data(), segments[2].data(),
331 segments[3].data(), pixels_per_frame);
332 simd::split_16bit_to_planes(b_plane.data(), segments[4].data(),
333 segments[5].data(), pixels_per_frame);
334 } else {
335 // Other sample counts: scalar fallback
336 for (size_t i = 0; i < pixels_per_frame; ++i) {
337 for (int s = 0; s < params.samples_per_pixel; ++s) {
338 size_t idx = (i * params.samples_per_pixel + s) * 2;
339 segments[s * 2].push_back(pixel_data[idx + 1]); // High byte
340 segments[s * 2 + 1].push_back(pixel_data[idx]); // Low byte
341 }
342 }
343 }
344 }
345
346 // Encode each segment
347 std::vector<std::vector<uint8_t>> encoded_segments(num_segments);
348 for (int i = 0; i < num_segments; ++i) {
349 encode_rle_segment(segments[i], encoded_segments[i]);
350 }
351
352 // Build output with RLE header
353 std::vector<uint8_t> output;
354
355 // Calculate total size (header + all segments, each padded to even length)
356 size_t total_size = kRLEHeaderSize;
357 for (const auto& seg : encoded_segments) {
358 total_size += seg.size();
359 if (seg.size() % 2 != 0) {
360 ++total_size; // Padding byte
361 }
362 }
363 output.reserve(total_size);
364
365 // Write header (16 x 4-byte offsets)
366 output.resize(kRLEHeaderSize, 0);
367
368 // First 4 bytes: number of segments
369 write_le32(output.data(), static_cast<uint32_t>(num_segments));
370
371 // Calculate and write segment offsets
372 uint32_t current_offset = kRLEHeaderSize;
373 for (int i = 0; i < num_segments; ++i) {
374 write_le32(output.data() + 4 + i * 4, current_offset);
375 current_offset += static_cast<uint32_t>(encoded_segments[i].size());
376 if (encoded_segments[i].size() % 2 != 0) {
377 ++current_offset; // Account for padding
378 }
379 }
380
381 // Write encoded segments
382 for (const auto& seg : encoded_segments) {
383 output.insert(output.end(), seg.begin(), seg.end());
384 if (seg.size() % 2 != 0) {
385 output.push_back(0); // Padding byte
386 }
387 }
388
389 image_params output_params = params;
390 return kcenon::pacs::ok<compression_result>(compression_result{std::move(output), output_params});
391 }
392
394 std::span<const uint8_t> compressed_data,
395 const image_params& params) const {
396
397 const uint8_t* header = compressed_data.data();
398
399 // Read number of segments from header
400 uint32_t num_segments = read_le32(header);
401 if (num_segments == 0 || num_segments > kMaxSegments) {
403 "Invalid RLE segment count: " + std::to_string(num_segments));
404 }
405
406 int expected_segments = calculate_segment_count(params);
407 if (static_cast<int>(num_segments) != expected_segments) {
409 "Segment count mismatch: expected " + std::to_string(expected_segments) +
410 ", got " + std::to_string(num_segments));
411 }
412
413 // Read segment offsets
414 std::vector<uint32_t> offsets(num_segments);
415 for (uint32_t i = 0; i < num_segments; ++i) {
416 offsets[i] = read_le32(header + 4 + i * 4);
417 if (offsets[i] >= compressed_data.size()) {
419 "Invalid segment offset: " + std::to_string(offsets[i]));
420 }
421 }
422
423 // Calculate segment sizes
424 std::vector<size_t> sizes(num_segments);
425 for (uint32_t i = 0; i < num_segments; ++i) {
426 if (i + 1 < num_segments) {
427 sizes[i] = offsets[i + 1] - offsets[i];
428 } else {
429 sizes[i] = compressed_data.size() - offsets[i];
430 }
431 }
432
433 size_t pixels_per_frame = static_cast<size_t>(params.width) * params.height;
434 int bytes_per_sample = (params.bits_allocated + 7) / 8;
435
436 // Decode each segment
437 std::vector<std::vector<uint8_t>> decoded_segments(num_segments);
438 for (uint32_t i = 0; i < num_segments; ++i) {
439 std::span<const uint8_t> segment_data(
440 compressed_data.data() + offsets[i], sizes[i]);
441 decoded_segments[i] = decode_rle_segment(segment_data, pixels_per_frame);
442
443 if (decoded_segments[i].size() != pixels_per_frame) {
445 "Segment " + std::to_string(i) + " decoded size mismatch: expected " +
446 std::to_string(pixels_per_frame) + ", got " +
447 std::to_string(decoded_segments[i].size()));
448 }
449 }
450
451 // Reconstruct pixel data from segments using SIMD optimization
452 size_t output_size = pixels_per_frame * params.samples_per_pixel * bytes_per_sample;
453 std::vector<uint8_t> output(output_size);
454
455 if (bytes_per_sample == 1) {
456 // 8-bit samples
457 if (params.samples_per_pixel == 1) {
458 // Grayscale: direct move
459 output = std::move(decoded_segments[0]);
460 } else if (params.samples_per_pixel == 3) {
461 // Color (RGB): interleave samples using SIMD
463 decoded_segments[0].data(),
464 decoded_segments[1].data(),
465 decoded_segments[2].data(),
466 output.data(),
467 pixels_per_frame);
468 } else {
469 // Other sample counts: scalar fallback
470 for (size_t i = 0; i < pixels_per_frame; ++i) {
471 for (int s = 0; s < params.samples_per_pixel; ++s) {
472 output[i * params.samples_per_pixel + s] = decoded_segments[s][i];
473 }
474 }
475 }
476 } else {
477 // 16-bit samples
478 if (params.samples_per_pixel == 1) {
479 // Grayscale: combine high and low byte segments using SIMD
481 decoded_segments[0].data(), // High byte
482 decoded_segments[1].data(), // Low byte
483 output.data(),
484 pixels_per_frame);
485 } else if (params.samples_per_pixel == 3) {
486 // 16-bit Color: merge each color component, then interleave
487 std::vector<uint8_t> r_plane(pixels_per_frame * 2);
488 std::vector<uint8_t> g_plane(pixels_per_frame * 2);
489 std::vector<uint8_t> b_plane(pixels_per_frame * 2);
490
491 // Merge high/low bytes for each color using SIMD
492 simd::merge_planes_to_16bit(decoded_segments[0].data(),
493 decoded_segments[1].data(),
494 r_plane.data(), pixels_per_frame);
495 simd::merge_planes_to_16bit(decoded_segments[2].data(),
496 decoded_segments[3].data(),
497 g_plane.data(), pixels_per_frame);
498 simd::merge_planes_to_16bit(decoded_segments[4].data(),
499 decoded_segments[5].data(),
500 b_plane.data(), pixels_per_frame);
501
502 // Interleave RGB (scalar for now, 16-bit RGB is rare)
503 for (size_t i = 0; i < pixels_per_frame; ++i) {
504 size_t src_idx = i * 2;
505 size_t dst_idx = i * 6; // 3 samples * 2 bytes
506 output[dst_idx] = r_plane[src_idx];
507 output[dst_idx + 1] = r_plane[src_idx + 1];
508 output[dst_idx + 2] = g_plane[src_idx];
509 output[dst_idx + 3] = g_plane[src_idx + 1];
510 output[dst_idx + 4] = b_plane[src_idx];
511 output[dst_idx + 5] = b_plane[src_idx + 1];
512 }
513 } else {
514 // Other sample counts: scalar fallback
515 for (size_t i = 0; i < pixels_per_frame; ++i) {
516 for (int s = 0; s < params.samples_per_pixel; ++s) {
517 size_t idx = (i * params.samples_per_pixel + s) * 2;
518 output[idx] = decoded_segments[s * 2 + 1][i]; // Low byte
519 output[idx + 1] = decoded_segments[s * 2][i]; // High byte
520 }
521 }
522 }
523 }
524
525 // Build output parameters
526 image_params output_params;
527 output_params.width = params.width;
528 output_params.height = params.height;
529 output_params.bits_allocated = params.bits_allocated;
530 output_params.bits_stored = params.bits_stored > 0 ? params.bits_stored : params.bits_allocated;
531 output_params.high_bit = output_params.bits_stored - 1;
532 output_params.samples_per_pixel = params.samples_per_pixel;
533 output_params.planar_configuration = 0; // Always interleaved output
534 output_params.pixel_representation = params.pixel_representation;
535 output_params.photometric = params.photometric;
536
537 return kcenon::pacs::ok<compression_result>(compression_result{std::move(output), output_params});
538 }
539};
540
541// rle_codec implementation
542
543rle_codec::rle_codec() : impl_(std::make_unique<impl>()) {}
544
545rle_codec::~rle_codec() = default;
546
547rle_codec::rle_codec(rle_codec&&) noexcept = default;
548
549rle_codec& rle_codec::operator=(rle_codec&&) noexcept = default;
550
551std::string_view rle_codec::transfer_syntax_uid() const noexcept {
552 return kTransferSyntaxUID;
553}
554
555std::string_view rle_codec::name() const noexcept {
556 return "RLE Lossless";
557}
558
559bool rle_codec::is_lossy() const noexcept {
560 return false;
561}
562
563bool rle_codec::can_encode(const image_params& params) const noexcept {
564 // RLE supports 8-bit and 16-bit samples
565 if (params.bits_allocated != 8 && params.bits_allocated != 16) {
566 return false;
567 }
568 // 1-3 samples per pixel
569 if (params.samples_per_pixel < 1 || params.samples_per_pixel > 3) {
570 return false;
571 }
572 // Check segment count limit
573 int bytes_per_sample = (params.bits_allocated + 7) / 8;
574 int num_segments = params.samples_per_pixel * bytes_per_sample;
575 if (num_segments > kMaxSegments) {
576 return false;
577 }
578 // Valid dimensions
579 if (params.width == 0 || params.height == 0) {
580 return false;
581 }
582 return true;
583}
584
585bool rle_codec::can_decode(const image_params& params) const noexcept {
586 // Can decode if parameters are valid for RLE
587 // Width/height can be 0 (unknown) for partial validation
588 if (params.bits_allocated != 0 &&
589 params.bits_allocated != 8 &&
590 params.bits_allocated != 16) {
591 return false;
592 }
593 if (params.samples_per_pixel != 0 &&
594 (params.samples_per_pixel < 1 || params.samples_per_pixel > 3)) {
595 return false;
596 }
597 return true;
598}
599
601 std::span<const uint8_t> pixel_data,
602 const image_params& params,
603 const compression_options& options) const {
604 return impl_->encode(pixel_data, params, options);
605}
606
608 std::span<const uint8_t> compressed_data,
609 const image_params& params) const {
610 return impl_->decode(compressed_data, params);
611}
612
613} // namespace kcenon::pacs::encoding::compression
PIMPL implementation for rle_codec.
codec_result decode_frame(std::span< const uint8_t > compressed_data, const image_params &params) const
codec_result encode(std::span< const uint8_t > pixel_data, const image_params &params, const compression_options &options) const
codec_result decode(std::span< const uint8_t > compressed_data, const image_params &params) const
bool valid_for_rle(const image_params &params) const noexcept
codec_result encode_frame(std::span< const uint8_t > pixel_data, const image_params &params) const
DICOM RLE Lossless codec implementation.
Definition rle_codec.h:49
std::string_view name() const noexcept override
Returns a human-readable name for the codec.
bool can_encode(const image_params &params) const noexcept override
Checks if this codec supports the given image parameters.
rle_codec()
Constructs an RLE codec instance.
codec_result decode(std::span< const uint8_t > compressed_data, const image_params &params) const override
Decompresses RLE data.
static constexpr size_t kRLEHeaderSize
RLE header size (64 bytes: 16 x 4-byte offsets)
Definition rle_codec.h:58
codec_result encode(std::span< const uint8_t > pixel_data, const image_params &params, const compression_options &options={}) const override
Compresses pixel data to RLE format.
bool can_decode(const image_params &params) const noexcept override
Checks if this codec can decode data with given parameters.
static constexpr int kMaxSegments
Maximum number of RLE segments allowed by DICOM specification.
Definition rle_codec.h:55
bool is_lossy() const noexcept override
Checks if this codec produces lossy compression.
void merge_planes_to_16bit(const uint8_t *high, const uint8_t *low, uint8_t *dst, size_t pixel_count) noexcept
Merge high and low byte planes into 16-bit data.
Definition simd_rle.h:837
void interleaved_to_planar_rgb8(const uint8_t *src, uint8_t *r, uint8_t *g, uint8_t *b, size_t pixel_count) noexcept
Convert interleaved RGB to planar format using best available SIMD.
Definition simd_rle.h:725
void planar_to_interleaved_rgb8(const uint8_t *r, const uint8_t *g, const uint8_t *b, uint8_t *dst, size_t pixel_count) noexcept
Convert planar RGB to interleaved format using best available SIMD.
Definition simd_rle.h:763
void split_16bit_to_planes(const uint8_t *src, uint8_t *high, uint8_t *low, size_t pixel_count) noexcept
Split 16-bit data into high and low byte planes.
Definition simd_rle.h:800
constexpr int decompression_error
Definition result.h:79
@ control
Internal pipeline control messages.
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
Result<T> type aliases and helpers for PACS system.
SIMD optimizations for RLE codec operations.
Compression quality settings for lossy codecs.
Successful result of a compression/decompression operation.
Parameters describing image pixel data.
uint16_t samples_per_pixel
Number of samples per pixel (0028,0002) 1 for grayscale, 3 for color.
uint16_t bits_allocated
Bits allocated per pixel sample (0028,0100) Valid values: 8, 16.
uint16_t height
Image height in pixels (Rows - 0028,0010)
size_t frame_size_bytes() const noexcept
Calculates the size of uncompressed pixel data in bytes.
photometric_interpretation photometric
Photometric interpretation (0028,0004)
uint16_t planar_configuration
Planar configuration (0028,0006) 0 = interleaved (R1G1B1R2G2B2...), 1 = separate planes (RRR....
uint16_t pixel_representation
Pixel representation (0028,0103) 0 = unsigned, 1 = signed.
uint16_t width
Image width in pixels (Columns - 0028,0011)
uint16_t bits_stored
Bits stored per pixel sample (0028,0101) Must be <= bits_allocated.
uint16_t high_bit
High bit position (0028,0102) Typically bits_stored - 1.