PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
jpeg2000_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
7
8#include <algorithm>
9#include <cstring>
10#include <stdexcept>
11
12#ifdef PACS_WITH_JPEG2000_CODEC
13#include <openjpeg.h>
14#endif
15
17
18namespace {
19
20#ifdef PACS_WITH_JPEG2000_CODEC
21
25void opj_error_callback(const char* msg, void* client_data) {
26 auto* error_msg = static_cast<std::string*>(client_data);
27 if (error_msg && msg) {
28 if (!error_msg->empty()) {
29 error_msg->append("; ");
30 }
31 error_msg->append(msg);
32 // Remove trailing newline if present
33 while (!error_msg->empty() && error_msg->back() == '\n') {
34 error_msg->pop_back();
35 }
36 }
37}
38
43void opj_warning_callback([[maybe_unused]] const char* msg,
44 [[maybe_unused]] void* client_data) {
45 // Warnings are silently ignored in production
46 // Could be logged via logger_adapter if needed
47}
48
53void opj_info_callback([[maybe_unused]] const char* msg,
54 [[maybe_unused]] void* client_data) {
55 // Info messages are suppressed
56}
57
61struct opj_memory_stream {
62 const uint8_t* data;
63 size_t size;
64 size_t offset;
65};
66
70OPJ_SIZE_T opj_memory_stream_read(void* buffer, OPJ_SIZE_T nb_bytes, void* user_data) {
71 auto* stream = static_cast<opj_memory_stream*>(user_data);
72 if (!stream || stream->offset >= stream->size) {
73 return static_cast<OPJ_SIZE_T>(-1);
74 }
75
76 OPJ_SIZE_T bytes_to_read = (std::min)(nb_bytes,
77 static_cast<OPJ_SIZE_T>(stream->size - stream->offset));
78 std::memcpy(buffer, stream->data + stream->offset, bytes_to_read);
79 stream->offset += bytes_to_read;
80 return bytes_to_read;
81}
82
86OPJ_OFF_T opj_memory_stream_skip(OPJ_OFF_T nb_bytes, void* user_data) {
87 auto* stream = static_cast<opj_memory_stream*>(user_data);
88 if (!stream) {
89 return -1;
90 }
91
92 if (nb_bytes < 0) {
93 // Handle negative skip (backward)
94 if (stream->offset < static_cast<size_t>(-nb_bytes)) {
95 nb_bytes = -static_cast<OPJ_OFF_T>(stream->offset);
96 }
97 } else {
98 // Handle positive skip (forward)
99 if (stream->offset + nb_bytes > stream->size) {
100 nb_bytes = static_cast<OPJ_OFF_T>(stream->size - stream->offset);
101 }
102 }
103
104 stream->offset = static_cast<size_t>(static_cast<OPJ_OFF_T>(stream->offset) + nb_bytes);
105 return nb_bytes;
106}
107
111OPJ_BOOL opj_memory_stream_seek(OPJ_OFF_T nb_bytes, void* user_data) {
112 auto* stream = static_cast<opj_memory_stream*>(user_data);
113 if (!stream || nb_bytes < 0 || static_cast<size_t>(nb_bytes) > stream->size) {
114 return OPJ_FALSE;
115 }
116 stream->offset = static_cast<size_t>(nb_bytes);
117 return OPJ_TRUE;
118}
119
123struct opj_output_buffer {
124 std::vector<uint8_t> data;
125 size_t offset;
126};
127
131OPJ_SIZE_T opj_output_buffer_write(void* buffer, OPJ_SIZE_T nb_bytes, void* user_data) {
132 auto* output = static_cast<opj_output_buffer*>(user_data);
133 if (!output || !buffer) {
134 return static_cast<OPJ_SIZE_T>(-1);
135 }
136
137 size_t new_size = output->offset + nb_bytes;
138 if (new_size > output->data.size()) {
139 output->data.resize(new_size);
140 }
141
142 std::memcpy(output->data.data() + output->offset, buffer, nb_bytes);
143 output->offset += nb_bytes;
144 return nb_bytes;
145}
146
150OPJ_OFF_T opj_output_buffer_skip(OPJ_OFF_T nb_bytes, void* user_data) {
151 auto* output = static_cast<opj_output_buffer*>(user_data);
152 if (!output || nb_bytes < 0) {
153 return -1;
154 }
155
156 size_t new_offset = output->offset + static_cast<size_t>(nb_bytes);
157 if (new_offset > output->data.size()) {
158 output->data.resize(new_offset);
159 }
160 output->offset = new_offset;
161 return nb_bytes;
162}
163
167OPJ_BOOL opj_output_buffer_seek(OPJ_OFF_T nb_bytes, void* user_data) {
168 auto* output = static_cast<opj_output_buffer*>(user_data);
169 if (!output || nb_bytes < 0) {
170 return OPJ_FALSE;
171 }
172
173 size_t new_offset = static_cast<size_t>(nb_bytes);
174 if (new_offset > output->data.size()) {
175 output->data.resize(new_offset);
176 }
177 output->offset = new_offset;
178 return OPJ_TRUE;
179}
180
185OPJ_CODEC_FORMAT detect_j2k_format(std::span<const uint8_t> data) {
186 if (data.size() < 12) {
187 return OPJ_CODEC_J2K; // Default to raw codestream
188 }
189
190 // JP2 signature: 00 00 00 0C 6A 50 20 20 0D 0A 87 0A
191 static constexpr uint8_t jp2_signature[] = {
192 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A
193 };
194
195 if (std::memcmp(data.data(), jp2_signature, sizeof(jp2_signature)) == 0) {
196 return OPJ_CODEC_JP2;
197 }
198
199 // J2K codestream signature: FF 4F FF 51 (SOC + SIZ markers)
200 if (data[0] == 0xFF && data[1] == 0x4F) {
201 return OPJ_CODEC_J2K;
202 }
203
204 return OPJ_CODEC_J2K; // Default
205}
206
207#endif // PACS_WITH_JPEG2000_CODEC
208
209} // namespace
210
215public:
216 explicit impl(bool lossless, float compression_ratio, int resolution_levels)
217 : lossless_(lossless)
219 , resolution_levels_(std::clamp(resolution_levels, 1, 32)) {}
220
221 [[nodiscard]] bool is_lossless_mode() const noexcept {
222 return lossless_;
223 }
224
225 [[nodiscard]] float compression_ratio() const noexcept {
226 return compression_ratio_;
227 }
228
229 [[nodiscard]] int resolution_levels() const noexcept {
230 return resolution_levels_;
231 }
232
233 [[nodiscard]] codec_result encode(std::span<const uint8_t> pixel_data,
234 const image_params& params,
235 const compression_options& options) const {
236#ifndef PACS_WITH_JPEG2000_CODEC
237 (void)pixel_data;
238 (void)params;
239 (void)options;
241 "JPEG 2000 codec not available: OpenJPEG library not found at build time");
242#else
243 // Determine if we should use lossless mode
244 bool use_lossless = lossless_ || options.lossless;
245
246 // Create image component parameters
247 std::vector<opj_image_cmptparm_t> cmptparm(params.samples_per_pixel);
248 for (uint16_t i = 0; i < params.samples_per_pixel; ++i) {
249 std::memset(&cmptparm[i], 0, sizeof(opj_image_cmptparm_t));
250 cmptparm[i].prec = params.bits_stored;
251 cmptparm[i].bpp = params.bits_stored;
252 cmptparm[i].sgnd = params.is_signed() ? 1 : 0;
253 cmptparm[i].dx = 1;
254 cmptparm[i].dy = 1;
255 cmptparm[i].w = params.width;
256 cmptparm[i].h = params.height;
257 }
258
259 // Determine color space
260 OPJ_COLOR_SPACE color_space = OPJ_CLRSPC_GRAY;
261 if (params.samples_per_pixel == 3) {
262 color_space = (params.photometric == photometric_interpretation::rgb)
263 ? OPJ_CLRSPC_SRGB
264 : OPJ_CLRSPC_SYCC;
265 }
266
267 // Create image
268 opj_image_t* image = opj_image_create(
269 params.samples_per_pixel,
270 cmptparm.data(),
271 color_space);
272
273 if (!image) {
274 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to create OpenJPEG image structure");
275 }
276
277 // Set image offset and reference grid
278 image->x0 = 0;
279 image->y0 = 0;
280 image->x1 = params.width;
281 image->y1 = params.height;
282
283 // Copy pixel data to image components
284 size_t bytes_per_sample = (params.bits_allocated + 7) / 8;
285 size_t pixel_count = static_cast<size_t>(params.width) * params.height;
286
287 for (uint16_t c = 0; c < params.samples_per_pixel; ++c) {
288 OPJ_INT32* comp_data = image->comps[c].data;
289
290 for (size_t i = 0; i < pixel_count; ++i) {
291 size_t src_idx;
292 if (params.planar_configuration == 0) {
293 // Interleaved: R0G0B0R1G1B1...
294 src_idx = (i * params.samples_per_pixel + c) * bytes_per_sample;
295 } else {
296 // Planar: RRR...GGG...BBB...
297 src_idx = (c * pixel_count + i) * bytes_per_sample;
298 }
299
300 if (src_idx + bytes_per_sample > pixel_data.size()) {
301 opj_image_destroy(image);
303 }
304
305 OPJ_INT32 value = 0;
306 if (bytes_per_sample == 1) {
307 value = params.is_signed()
308 ? static_cast<OPJ_INT32>(static_cast<int8_t>(pixel_data[src_idx]))
309 : static_cast<OPJ_INT32>(pixel_data[src_idx]);
310 } else {
311 // 16-bit (little-endian)
312 uint16_t raw = static_cast<uint16_t>(pixel_data[src_idx]) |
313 (static_cast<uint16_t>(pixel_data[src_idx + 1]) << 8);
314 value = params.is_signed()
315 ? static_cast<OPJ_INT32>(static_cast<int16_t>(raw))
316 : static_cast<OPJ_INT32>(raw);
317 }
318 comp_data[i] = value;
319 }
320 }
321
322 // Create encoder
323 opj_codec_t* codec = opj_create_compress(OPJ_CODEC_J2K);
324 if (!codec) {
325 opj_image_destroy(image);
327 }
328
329 // Set up error handling
330 std::string error_msg;
331 opj_set_error_handler(codec, opj_error_callback, &error_msg);
332 opj_set_warning_handler(codec, opj_warning_callback, nullptr);
333 opj_set_info_handler(codec, opj_info_callback, nullptr);
334
335 // Set encoding parameters
336 opj_cparameters_t parameters;
337 opj_set_default_encoder_parameters(&parameters);
338
339 parameters.tcp_numlayers = 1;
340 parameters.cp_disto_alloc = 1;
341 parameters.numresolution = resolution_levels_;
342
343 if (use_lossless) {
344 // Lossless: use reversible 5/3 wavelet
345 parameters.irreversible = 0;
346 parameters.tcp_rates[0] = 0; // 0 = lossless
347 } else {
348 // Lossy: use irreversible 9/7 wavelet
349 parameters.irreversible = 1;
350
351 // Convert quality (1-100) to compression ratio
352 float ratio = compression_ratio_;
353 if (options.quality > 0 && options.quality <= 100) {
354 // Map quality 100->2 (near lossless), quality 1->100 (high compression)
355 ratio = 2.0f + (100.0f - static_cast<float>(options.quality)) * 0.98f;
356 }
357 parameters.tcp_rates[0] = ratio;
358 }
359
360 // Setup encoder with parameters
361 if (!opj_setup_encoder(codec, &parameters, image)) {
362 opj_destroy_codec(codec);
363 opj_image_destroy(image);
364 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to setup OpenJPEG encoder: " + error_msg);
365 }
366
367 // Create output stream
368 opj_output_buffer output_buf;
369 output_buf.offset = 0;
370 output_buf.data.reserve(pixel_data.size() / 2); // Estimate compressed size
371
372 opj_stream_t* stream = opj_stream_default_create(OPJ_FALSE);
373 if (!stream) {
374 opj_destroy_codec(codec);
375 opj_image_destroy(image);
377 }
378
379 opj_stream_set_user_data(stream, &output_buf, nullptr);
380 opj_stream_set_user_data_length(stream, 0);
381 opj_stream_set_write_function(stream, opj_output_buffer_write);
382 opj_stream_set_skip_function(stream, opj_output_buffer_skip);
383 opj_stream_set_seek_function(stream, opj_output_buffer_seek);
384
385 // Encode
386 if (!opj_start_compress(codec, image, stream)) {
387 opj_stream_destroy(stream);
388 opj_destroy_codec(codec);
389 opj_image_destroy(image);
390 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to start JPEG 2000 encoding: " + error_msg);
391 }
392
393 if (!opj_encode(codec, stream)) {
394 opj_stream_destroy(stream);
395 opj_destroy_codec(codec);
396 opj_image_destroy(image);
397 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to encode JPEG 2000 data: " + error_msg);
398 }
399
400 if (!opj_end_compress(codec, stream)) {
401 opj_stream_destroy(stream);
402 opj_destroy_codec(codec);
403 opj_image_destroy(image);
404 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to finalize JPEG 2000 encoding: " + error_msg);
405 }
406
407 // Cleanup
408 opj_stream_destroy(stream);
409 opj_destroy_codec(codec);
410 opj_image_destroy(image);
411
412 // Trim output buffer to actual size
413 output_buf.data.resize(output_buf.offset);
414
415 return kcenon::pacs::ok<compression_result>(compression_result{std::move(output_buf.data), params});
416#endif // PACS_WITH_JPEG2000_CODEC
417 }
418
419 [[nodiscard]] codec_result decode(std::span<const uint8_t> compressed_data,
420 const image_params& params) const {
421#ifndef PACS_WITH_JPEG2000_CODEC
422 (void)compressed_data;
423 (void)params;
425 "JPEG 2000 codec not available: OpenJPEG library not found at build time");
426#else
427 if (compressed_data.empty()) {
429 }
430
431 // Detect format
432 OPJ_CODEC_FORMAT format = detect_j2k_format(compressed_data);
433
434 // Create decoder
435 opj_codec_t* codec = opj_create_decompress(format);
436 if (!codec) {
438 }
439
440 // Set up error handling
441 std::string error_msg;
442 opj_set_error_handler(codec, opj_error_callback, &error_msg);
443 opj_set_warning_handler(codec, opj_warning_callback, nullptr);
444 opj_set_info_handler(codec, opj_info_callback, nullptr);
445
446 // Set decoding parameters
447 opj_dparameters_t parameters;
448 opj_set_default_decoder_parameters(&parameters);
449
450 if (!opj_setup_decoder(codec, &parameters)) {
451 opj_destroy_codec(codec);
452 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to setup OpenJPEG decoder: " + error_msg);
453 }
454
455 // Create input stream
456 opj_memory_stream input_stream;
457 input_stream.data = compressed_data.data();
458 input_stream.size = compressed_data.size();
459 input_stream.offset = 0;
460
461 opj_stream_t* stream = opj_stream_default_create(OPJ_TRUE);
462 if (!stream) {
463 opj_destroy_codec(codec);
465 }
466
467 opj_stream_set_user_data(stream, &input_stream, nullptr);
468 opj_stream_set_user_data_length(stream, compressed_data.size());
469 opj_stream_set_read_function(stream, opj_memory_stream_read);
470 opj_stream_set_skip_function(stream, opj_memory_stream_skip);
471 opj_stream_set_seek_function(stream, opj_memory_stream_seek);
472
473 // Read header
474 opj_image_t* image = nullptr;
475 if (!opj_read_header(stream, codec, &image)) {
476 opj_stream_destroy(stream);
477 opj_destroy_codec(codec);
478 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to read JPEG 2000 header: " + error_msg);
479 }
480
481 // Decode
482 if (!opj_decode(codec, stream, image)) {
483 opj_image_destroy(image);
484 opj_stream_destroy(stream);
485 opj_destroy_codec(codec);
486 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to decode JPEG 2000 data: " + error_msg);
487 }
488
489 if (!opj_end_decompress(codec, stream)) {
490 opj_image_destroy(image);
491 opj_stream_destroy(stream);
492 opj_destroy_codec(codec);
493 return kcenon::pacs::pacs_error<compression_result>(kcenon::pacs::error_codes::decompression_error, "Failed to finalize JPEG 2000 decoding: " + error_msg);
494 }
495
496 // Extract image parameters from decoded data
497 image_params output_params = params;
498 output_params.width = static_cast<uint16_t>(image->x1 - image->x0);
499 output_params.height = static_cast<uint16_t>(image->y1 - image->y0);
500 output_params.samples_per_pixel = static_cast<uint16_t>(image->numcomps);
501
502 if (image->numcomps > 0) {
503 output_params.bits_stored = static_cast<uint16_t>(image->comps[0].prec);
504 output_params.bits_allocated = (output_params.bits_stored <= 8) ? 8 : 16;
505 output_params.high_bit = output_params.bits_stored - 1;
506 output_params.pixel_representation = image->comps[0].sgnd ? 1 : 0;
507 }
508
509 // Determine photometric interpretation
510 if (image->numcomps == 1) {
512 } else if (image->numcomps == 3) {
513 output_params.photometric = (image->color_space == OPJ_CLRSPC_SRGB)
516 }
517
518 // Validate dimensions if provided
519 if (params.width > 0 && params.width != output_params.width) {
520 opj_image_destroy(image);
521 opj_stream_destroy(stream);
522 opj_destroy_codec(codec);
524 std::to_string(params.width) + ", got " +
525 std::to_string(output_params.width));
526 }
527 if (params.height > 0 && params.height != output_params.height) {
528 opj_image_destroy(image);
529 opj_stream_destroy(stream);
530 opj_destroy_codec(codec);
532 std::to_string(params.height) + ", got " +
533 std::to_string(output_params.height));
534 }
535
536 // Allocate output buffer
537 size_t pixel_count = static_cast<size_t>(output_params.width) * output_params.height;
538 size_t bytes_per_sample = (output_params.bits_allocated + 7) / 8;
539 size_t total_size = pixel_count * output_params.samples_per_pixel * bytes_per_sample;
540
541 std::vector<uint8_t> output_data(total_size);
542
543 // Copy decoded data to output buffer (interleaved format)
544 for (OPJ_UINT32 c = 0; c < image->numcomps; ++c) {
545 const OPJ_INT32* comp_data = image->comps[c].data;
546
547 for (size_t i = 0; i < pixel_count; ++i) {
548 size_t dst_idx = (i * output_params.samples_per_pixel + c) * bytes_per_sample;
549 OPJ_INT32 value = comp_data[i];
550
551 if (bytes_per_sample == 1) {
552 output_data[dst_idx] = static_cast<uint8_t>(value & 0xFF);
553 } else {
554 // 16-bit little-endian
555 output_data[dst_idx] = static_cast<uint8_t>(value & 0xFF);
556 output_data[dst_idx + 1] = static_cast<uint8_t>((value >> 8) & 0xFF);
557 }
558 }
559 }
560
561 // Cleanup
562 opj_image_destroy(image);
563 opj_stream_destroy(stream);
564 opj_destroy_codec(codec);
565
566 output_params.planar_configuration = 0; // Output is always interleaved
567 return kcenon::pacs::ok<compression_result>(compression_result{std::move(output_data), output_params});
568#endif // PACS_WITH_JPEG2000_CODEC
569 }
570
571private:
575};
576
577// jpeg2000_codec implementation
578
580 float compression_ratio,
581 int resolution_levels)
582 : impl_(std::make_unique<impl>(lossless, compression_ratio, resolution_levels)) {}
583
585
587jpeg2000_codec& jpeg2000_codec::operator=(jpeg2000_codec&&) noexcept = default;
588
589std::string_view jpeg2000_codec::transfer_syntax_uid() const noexcept {
590 return impl_->is_lossless_mode() ? kTransferSyntaxUIDLossless : kTransferSyntaxUIDLossy;
591}
592
593std::string_view jpeg2000_codec::name() const noexcept {
594 return impl_->is_lossless_mode() ? "JPEG 2000 Lossless" : "JPEG 2000";
595}
596
597bool jpeg2000_codec::is_lossy() const noexcept {
598 return !impl_->is_lossless_mode();
599}
600
601bool jpeg2000_codec::can_encode(const image_params& params) const noexcept {
602 // JPEG 2000 supports wide range of bit depths
603 if (params.bits_stored < 1 || params.bits_stored > 16) {
604 return false;
605 }
606
607 // bits_allocated must be 8 or 16
608 if (params.bits_allocated != 8 && params.bits_allocated != 16) {
609 return false;
610 }
611
612 // Support grayscale (1) and color (3) images
613 if (params.samples_per_pixel != 1 && params.samples_per_pixel != 3) {
614 return false;
615 }
616
617 // Require valid dimensions
618 if (params.width == 0 || params.height == 0) {
619 return false;
620 }
621
622 return true;
623}
624
625bool jpeg2000_codec::can_decode(const image_params& params) const noexcept {
626 // For decoding, we're more lenient as actual parameters come from the codestream
627 // Just validate samples_per_pixel if specified
628 if (params.samples_per_pixel != 0 &&
629 params.samples_per_pixel != 1 &&
630 params.samples_per_pixel != 3) {
631 return false;
632 }
633 return true;
634}
635
636bool jpeg2000_codec::is_lossless_mode() const noexcept {
637 return impl_->is_lossless_mode();
638}
639
640float jpeg2000_codec::compression_ratio() const noexcept {
641 return impl_->compression_ratio();
642}
643
645 return impl_->resolution_levels();
646}
647
648codec_result jpeg2000_codec::encode(std::span<const uint8_t> pixel_data,
649 const image_params& params,
650 const compression_options& options) const {
651 return impl_->encode(pixel_data, params, options);
652}
653
654codec_result jpeg2000_codec::decode(std::span<const uint8_t> compressed_data,
655 const image_params& params) const {
656 return impl_->decode(compressed_data, params);
657}
658
659} // namespace kcenon::pacs::encoding::compression
codec_result encode(std::span< const uint8_t > pixel_data, const image_params &params, const compression_options &options) const
impl(bool lossless, float compression_ratio, int resolution_levels)
codec_result decode(std::span< const uint8_t > compressed_data, const image_params &params) const
JPEG 2000 codec implementation supporting both lossless and lossy modes.
bool is_lossy() const noexcept override
Checks if this codec produces lossy compression.
bool can_encode(const image_params &params) const noexcept override
Checks if this codec supports the given image parameters.
codec_result encode(std::span< const uint8_t > pixel_data, const image_params &params, const compression_options &options={}) const override
Compresses pixel data to JPEG 2000 format.
bool can_decode(const image_params &params) const noexcept override
Checks if this codec can decode data with given parameters.
float compression_ratio() const noexcept
Gets the current compression ratio setting.
jpeg2000_codec(bool lossless=true, float compression_ratio=kDefaultCompressionRatio, int resolution_levels=kDefaultResolutionLevels)
Constructs a JPEG 2000 codec instance.
codec_result decode(std::span< const uint8_t > compressed_data, const image_params &params) const override
Decompresses JPEG 2000 data.
int resolution_levels() const noexcept
Gets the number of DWT resolution levels.
std::string_view name() const noexcept override
Returns a human-readable name for the codec.
bool is_lossless_mode() const noexcept
Checks if this codec is configured for lossless mode.
@ monochrome2
Minimum pixel value displayed as black.
constexpr int decompression_error
Definition result.h:79
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.
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)
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.
bool is_signed() const noexcept
Checks if pixel values are signed integers.
uint16_t high_bit
High bit position (0028,0102) Typically bits_stored - 1.