PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
jpeg_ls_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_JPEGLS_CODEC
13#include <charls/charls.h>
14#endif
15
17
18namespace {
19
20#ifdef PACS_WITH_JPEGLS_CODEC
21
22// CharLS 2.x uses jpegls_error::what() for error messages
23
27[[nodiscard]] charls::interleave_mode get_interleave_mode(const image_params& params) {
28 if (params.samples_per_pixel == 1) {
29 return charls::interleave_mode::none;
30 }
31 // For color images, use sample interleaved mode (RGBRGB...)
32 return (params.planar_configuration == 0)
33 ? charls::interleave_mode::sample
34 : charls::interleave_mode::line;
35}
36
40[[nodiscard]] charls::color_transformation get_color_transform(const image_params& params) {
41 if (params.samples_per_pixel == 1) {
42 return charls::color_transformation::none;
43 }
44 // For RGB images, no color transformation is applied
45 // (JPEG-LS can optionally use HP color transforms, but DICOM typically doesn't)
46 return charls::color_transformation::none;
47}
48
49#endif // PACS_WITH_JPEGLS_CODEC
50
51} // namespace
52
57public:
58 explicit impl(bool lossless, int near_value)
59 : lossless_(lossless)
60 , near_value_(0) {
61 // Handle sentinel value: auto-determine NEAR based on mode
64 }
65 // Lossless mode always forces NEAR=0 (DICOM compliance)
66 else if (lossless_) {
68 }
69 // Explicit NEAR=0 forces lossless mode
70 else if (near_value <= 0) {
71 lossless_ = true;
73 }
74 // Near-lossless with explicit NEAR value
75 else {
76 near_value_ = std::clamp(near_value, 1, kMaxNearValue);
77 }
78 }
79
80 [[nodiscard]] bool is_lossless_mode() const noexcept {
81 return lossless_;
82 }
83
84 [[nodiscard]] int near_value() const noexcept {
85 return near_value_;
86 }
87
88 [[nodiscard]] codec_result encode(std::span<const uint8_t> pixel_data,
89 const image_params& params,
90 const compression_options& options) const {
91#ifndef PACS_WITH_JPEGLS_CODEC
92 (void)pixel_data;
93 (void)params;
94 (void)options;
96 "JPEG-LS codec not available: CharLS library not found at build time");
97#else
98 // Validate input
99 if (pixel_data.empty()) {
101 }
102
103 if (params.width == 0 || params.height == 0) {
105 }
106
107 // Determine effective NEAR value
108 int effective_near = near_value_;
109 bool use_lossless = lossless_ || options.lossless;
110
111 if (use_lossless) {
112 effective_near = 0;
113 } else if (!lossless_ && options.quality > 0 && options.quality <= 100) {
114 // Map quality (1-100) to NEAR parameter
115 // quality 100 -> NEAR 0 (lossless)
116 // quality 1 -> NEAR ~10 (more compression)
117 effective_near = static_cast<int>((100 - options.quality) * 10 / 100);
118 effective_near = std::clamp(effective_near, 0, kMaxNearValue);
119 }
120
121 try {
122 // Create encoder
123 charls::jpegls_encoder encoder;
124
125 // Set frame info
126 charls::frame_info frame_info{};
127 frame_info.width = params.width;
128 frame_info.height = params.height;
129 frame_info.bits_per_sample = params.bits_stored;
130 frame_info.component_count = params.samples_per_pixel;
131
132 encoder.frame_info(frame_info);
133
134 // Set interleave mode
135 encoder.interleave_mode(get_interleave_mode(params));
136
137 // Set NEAR parameter (0 = lossless)
138 encoder.near_lossless(effective_near);
139
140 // Set color transformation
141 encoder.color_transformation(get_color_transform(params));
142
143 // Calculate destination buffer size
144 // For high-entropy data, JPEG-LS output may exceed input size.
145 // Use the maximum of: CharLS estimate, input size + 20%, or input + 1KB overhead.
146 size_t estimated_size = encoder.estimated_destination_size();
147 size_t safe_size = (std::max)({
148 estimated_size,
149 pixel_data.size() + pixel_data.size() / 5, // input + 20%
150 pixel_data.size() + 1024 // input + 1KB header overhead
151 });
152 std::vector<uint8_t> destination(safe_size);
153
154 // Encode
155 encoder.destination(destination);
156 size_t bytes_written = encoder.encode(pixel_data);
157
158 // Resize to actual size
159 destination.resize(bytes_written);
160
161 return kcenon::pacs::ok<compression_result>(compression_result{std::move(destination), params});
162
163 } catch (const charls::jpegls_error& e) {
165 std::string("JPEG-LS encoding failed: ") + e.what());
166 } catch (const std::exception& e) {
168 std::string("JPEG-LS encoding failed: ") + e.what());
169 }
170#endif // PACS_WITH_JPEGLS_CODEC
171 }
172
173 [[nodiscard]] codec_result decode(std::span<const uint8_t> compressed_data,
174 const image_params& params) const {
175#ifndef PACS_WITH_JPEGLS_CODEC
176 (void)compressed_data;
177 (void)params;
179 "JPEG-LS codec not available: CharLS library not found at build time");
180#else
181 if (compressed_data.empty()) {
183 }
184
185 try {
186 // Create decoder
187 charls::jpegls_decoder decoder;
188 decoder.source(compressed_data);
189
190 // Read header to get frame info
191 decoder.read_header();
192
193 // Get frame info
194 const charls::frame_info& frame_info = decoder.frame_info();
195
196 // Build output parameters
197 image_params output_params = params;
198 output_params.width = static_cast<uint16_t>(frame_info.width);
199 output_params.height = static_cast<uint16_t>(frame_info.height);
200 output_params.bits_stored = static_cast<uint16_t>(frame_info.bits_per_sample);
201 output_params.bits_allocated = (frame_info.bits_per_sample <= 8) ? 8 : 16;
202 output_params.high_bit = output_params.bits_stored - 1;
203 output_params.samples_per_pixel = static_cast<uint16_t>(frame_info.component_count);
204
205 // Determine photometric interpretation
206 if (frame_info.component_count == 1) {
208 } else if (frame_info.component_count == 3) {
210 }
211
212 // Validate dimensions if provided
213 if (params.width > 0 && params.width != output_params.width) {
215 std::to_string(params.width) + ", got " +
216 std::to_string(output_params.width));
217 }
218 if (params.height > 0 && params.height != output_params.height) {
220 std::to_string(params.height) + ", got " +
221 std::to_string(output_params.height));
222 }
223
224 // Allocate destination buffer
225 size_t destination_size = decoder.destination_size();
226 std::vector<uint8_t> destination(destination_size);
227
228 // Decode
229 decoder.decode(destination);
230
231 // Set output as interleaved
232 output_params.planar_configuration = 0;
233
234 return kcenon::pacs::ok<compression_result>(compression_result{std::move(destination), output_params});
235
236 } catch (const charls::jpegls_error& e) {
238 std::string("JPEG-LS decoding failed: ") + e.what());
239 } catch (const std::exception& e) {
241 std::string("JPEG-LS decoding failed: ") + e.what());
242 }
243#endif // PACS_WITH_JPEGLS_CODEC
244 }
245
246private:
249};
250
251// jpeg_ls_codec implementation
252
253jpeg_ls_codec::jpeg_ls_codec(bool lossless, int near_value)
254 : impl_(std::make_unique<impl>(lossless, near_value)) {}
255
257
258jpeg_ls_codec::jpeg_ls_codec(jpeg_ls_codec&&) noexcept = default;
259jpeg_ls_codec& jpeg_ls_codec::operator=(jpeg_ls_codec&&) noexcept = default;
260
261std::string_view jpeg_ls_codec::transfer_syntax_uid() const noexcept {
262 return impl_->is_lossless_mode() ? kTransferSyntaxUIDLossless : kTransferSyntaxUIDNearLossless;
263}
264
265std::string_view jpeg_ls_codec::name() const noexcept {
266 return impl_->is_lossless_mode() ? "JPEG-LS Lossless" : "JPEG-LS Near-Lossless";
267}
268
269bool jpeg_ls_codec::is_lossy() const noexcept {
270 return !impl_->is_lossless_mode();
271}
272
273bool jpeg_ls_codec::can_encode(const image_params& params) const noexcept {
274 // JPEG-LS supports 2-16 bit precision per component
275 if (params.bits_stored < 2 || params.bits_stored > 16) {
276 return false;
277 }
278
279 // bits_allocated must be 8 or 16
280 if (params.bits_allocated != 8 && params.bits_allocated != 16) {
281 return false;
282 }
283
284 // Support grayscale (1) and color (3) images
285 if (params.samples_per_pixel != 1 && params.samples_per_pixel != 3) {
286 return false;
287 }
288
289 // Require valid dimensions
290 if (params.width == 0 || params.height == 0) {
291 return false;
292 }
293
294 // Maximum dimension check (JPEG-LS limit)
295 if (params.width > 65535 || params.height > 65535) {
296 return false;
297 }
298
299 return true;
300}
301
302bool jpeg_ls_codec::can_decode(const image_params& params) const noexcept {
303 // For decoding, we're more lenient as actual parameters come from the bitstream
304 // Just validate samples_per_pixel if specified
305 if (params.samples_per_pixel != 0 &&
306 params.samples_per_pixel != 1 &&
307 params.samples_per_pixel != 3) {
308 return false;
309 }
310 return true;
311}
312
313bool jpeg_ls_codec::is_lossless_mode() const noexcept {
314 return impl_->is_lossless_mode();
315}
316
317int jpeg_ls_codec::near_value() const noexcept {
318 return impl_->near_value();
319}
320
321codec_result jpeg_ls_codec::encode(std::span<const uint8_t> pixel_data,
322 const image_params& params,
323 const compression_options& options) const {
324 return impl_->encode(pixel_data, params, options);
325}
326
327codec_result jpeg_ls_codec::decode(std::span<const uint8_t> compressed_data,
328 const image_params& params) const {
329 return impl_->decode(compressed_data, params);
330}
331
332} // namespace kcenon::pacs::encoding::compression
codec_result decode(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
JPEG-LS codec implementation supporting both lossless and near-lossless modes.
bool is_lossless_mode() const noexcept
Checks if this codec is configured for lossless mode.
static constexpr int kMaxNearValue
Maximum NEAR parameter value (higher = more compression, more loss)
jpeg_ls_codec(bool lossless=true, int near_value=kAutoNearValue)
Constructs a JPEG-LS codec instance.
static constexpr int kLosslessNearValue
NEAR parameter for lossless mode.
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-LS format.
static constexpr int kDefaultNearLosslessValue
Default NEAR parameter for near-lossless mode (visually lossless quality)
int near_value() const noexcept
Gets the NEAR parameter value.
codec_result decode(std::span< const uint8_t > compressed_data, const image_params &params) const override
Decompresses JPEG-LS data.
std::string_view name() const noexcept override
Returns a human-readable name for the codec.
static constexpr int kAutoNearValue
Sentinel value indicating "auto-determine NEAR based on mode".
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.
bool can_decode(const image_params &params) const noexcept override
Checks if this codec can decode data with given parameters.
@ 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 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.