PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
jpeg_baseline_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
6
8
9#include <algorithm>
10#include <stdexcept>
11
12// libjpeg-turbo headers (only when JPEG codec is enabled)
13#ifdef PACS_WITH_JPEG_CODEC
14#include <csetjmp>
15#include <cstdio>
16#include <jpeglib.h>
17#include <jerror.h>
18#endif
19
21
22namespace {
23
24#ifdef PACS_WITH_JPEG_CODEC
25
32struct jpeg_error_handler {
33 jpeg_error_mgr pub; // Public fields (must be first)
34 jmp_buf setjmp_buffer; // For return to caller
35 std::string error_message; // Captured error message
36};
37
41void jpeg_error_exit(j_common_ptr cinfo) {
42 auto* err = reinterpret_cast<jpeg_error_handler*>(cinfo->err);
43
44 // Format error message
45 char buffer[JMSG_LENGTH_MAX];
46 (*cinfo->err->format_message)(cinfo, buffer);
47 err->error_message = buffer;
48
49 // Return control to the setjmp point
50 std::longjmp(err->setjmp_buffer, 1);
51}
52
56void jpeg_output_message([[maybe_unused]] j_common_ptr cinfo) {
57 // Silently ignore warning messages
58}
59
63class jpeg_compressor {
64public:
65 jpeg_compressor() {
66 cinfo_.err = jpeg_std_error(&jerr_.pub);
67 jerr_.pub.error_exit = jpeg_error_exit;
68 jerr_.pub.output_message = jpeg_output_message;
69
70 jpeg_create_compress(&cinfo_);
71 }
72
73 ~jpeg_compressor() {
74 jpeg_destroy_compress(&cinfo_);
75 }
76
77 jpeg_compressor(const jpeg_compressor&) = delete;
78 jpeg_compressor& operator=(const jpeg_compressor&) = delete;
79
80 jpeg_compress_struct* operator->() { return &cinfo_; }
81 jpeg_compress_struct& get() { return cinfo_; }
82 jpeg_error_handler& error() { return jerr_; }
83
84private:
85 jpeg_compress_struct cinfo_{};
86 jpeg_error_handler jerr_{};
87};
88
92class jpeg_decompressor {
93public:
94 jpeg_decompressor() {
95 cinfo_.err = jpeg_std_error(&jerr_.pub);
96 jerr_.pub.error_exit = jpeg_error_exit;
97 jerr_.pub.output_message = jpeg_output_message;
98
99 jpeg_create_decompress(&cinfo_);
100 }
101
102 ~jpeg_decompressor() {
103 jpeg_destroy_decompress(&cinfo_);
104 }
105
106 jpeg_decompressor(const jpeg_decompressor&) = delete;
107 jpeg_decompressor& operator=(const jpeg_decompressor&) = delete;
108
109 jpeg_decompress_struct* operator->() { return &cinfo_; }
110 jpeg_decompress_struct& get() { return cinfo_; }
111 jpeg_error_handler& error() { return jerr_; }
112
113private:
114 jpeg_decompress_struct cinfo_{};
115 jpeg_error_handler jerr_{};
116};
117
118// Helper function for creating codec results
119codec_result make_compression_error(const std::string& message) {
122}
123
124codec_result make_decompression_error(const std::string& message) {
127}
128
129codec_result make_compression_ok(std::vector<uint8_t> data, const image_params& params) {
130 return kcenon::pacs::ok<compression_result>(compression_result{std::move(data), params});
131}
132
133#endif // PACS_WITH_JPEG_CODEC
134
135} // namespace
136
143public:
144 impl() = default;
145 ~impl() = default;
146
147 [[nodiscard]] codec_result encode(
148 std::span<const uint8_t> pixel_data,
149 const image_params& params,
150 const compression_options& options) const {
151#ifndef PACS_WITH_JPEG_CODEC
152 (void)pixel_data;
153 (void)params;
154 (void)options;
157 "JPEG Baseline codec not available: libjpeg-turbo not found at build time");
158#else
159 if (pixel_data.empty()) {
160 return make_compression_error("Empty pixel data");
161 }
162
163 if (!params.valid_for_jpeg_baseline()) {
164 return make_compression_error(
165 "Invalid parameters for JPEG Baseline: requires 8-bit depth");
166 }
167
168 size_t expected_size = params.frame_size_bytes();
169 if (pixel_data.size() != expected_size) {
170 return make_compression_error(
171 "Pixel data size mismatch: expected " + std::to_string(expected_size) +
172 ", got " + std::to_string(pixel_data.size()));
173 }
174
175 jpeg_compressor compressor;
176
177 // Setup error handling with setjmp
178 if (setjmp(compressor.error().setjmp_buffer)) {
179 return make_compression_error(
180 "JPEG compression failed: " + compressor.error().error_message);
181 }
182
183 // Configure memory destination
184 uint8_t* out_buffer = nullptr;
185 unsigned long out_size = 0;
186 jpeg_mem_dest(&compressor.get(), &out_buffer, &out_size);
187
188 // Set image parameters
189 compressor->image_width = params.width;
190 compressor->image_height = params.height;
191 compressor->input_components = static_cast<int>(params.samples_per_pixel);
192
193 if (params.is_grayscale()) {
194 compressor->in_color_space = JCS_GRAYSCALE;
195 } else {
196 // Assume input is RGB, libjpeg will convert to YCbCr
197 compressor->in_color_space = JCS_RGB;
198 }
199
200 jpeg_set_defaults(&compressor.get());
201
202 // Apply quality setting (1-100)
203 int quality = std::clamp(options.quality, 1, 100);
204 jpeg_set_quality(&compressor.get(), quality, TRUE);
205
206 // Configure chroma subsampling for color images
207 if (!params.is_grayscale()) {
208 switch (options.chroma_subsampling) {
209 case 0: // 4:4:4
210 compressor->comp_info[0].h_samp_factor = 1;
211 compressor->comp_info[0].v_samp_factor = 1;
212 compressor->comp_info[1].h_samp_factor = 1;
213 compressor->comp_info[1].v_samp_factor = 1;
214 compressor->comp_info[2].h_samp_factor = 1;
215 compressor->comp_info[2].v_samp_factor = 1;
216 break;
217 case 1: // 4:2:2
218 compressor->comp_info[0].h_samp_factor = 2;
219 compressor->comp_info[0].v_samp_factor = 1;
220 compressor->comp_info[1].h_samp_factor = 1;
221 compressor->comp_info[1].v_samp_factor = 1;
222 compressor->comp_info[2].h_samp_factor = 1;
223 compressor->comp_info[2].v_samp_factor = 1;
224 break;
225 case 2: // 4:2:0 (default)
226 default:
227 compressor->comp_info[0].h_samp_factor = 2;
228 compressor->comp_info[0].v_samp_factor = 2;
229 compressor->comp_info[1].h_samp_factor = 1;
230 compressor->comp_info[1].v_samp_factor = 1;
231 compressor->comp_info[2].h_samp_factor = 1;
232 compressor->comp_info[2].v_samp_factor = 1;
233 break;
234 }
235 }
236
237 // Start compression
238 jpeg_start_compress(&compressor.get(), TRUE);
239
240 // Process scanlines
241 JDIMENSION row_stride = params.width * params.samples_per_pixel;
242 std::vector<JSAMPROW> row_pointers(params.height);
243
244 for (JDIMENSION row = 0; row < params.height; ++row) {
245 row_pointers[row] = const_cast<JSAMPROW>(
246 pixel_data.data() + row * row_stride);
247 }
248
249 // Write all scanlines at once
250 jpeg_write_scanlines(&compressor.get(), row_pointers.data(),
251 static_cast<JDIMENSION>(params.height));
252
253 // Finish compression
254 jpeg_finish_compress(&compressor.get());
255
256 // Copy output to result
257 std::vector<uint8_t> result(out_buffer, out_buffer + out_size);
258
259 // Free libjpeg-allocated buffer
260 std::free(out_buffer);
261
262 // Create output params (same as input for compression)
263 image_params output_params = params;
264
265 return make_compression_ok(std::move(result), output_params);
266#endif // PACS_WITH_JPEG_CODEC
267 }
268
269 [[nodiscard]] codec_result decode(
270 std::span<const uint8_t> compressed_data,
271 const image_params& params) const {
272#ifndef PACS_WITH_JPEG_CODEC
273 (void)compressed_data;
274 (void)params;
277 "JPEG Baseline codec not available: libjpeg-turbo not found at build time");
278#else
279 if (compressed_data.empty()) {
280 return make_decompression_error("Empty compressed data");
281 }
282
283 jpeg_decompressor decompressor;
284
285 // Setup error handling with setjmp
286 if (setjmp(decompressor.error().setjmp_buffer)) {
287 return make_decompression_error(
288 "JPEG decompression failed: " + decompressor.error().error_message);
289 }
290
291 // Setup memory source
292 // Note: Some libjpeg versions (e.g., Mono.framework on macOS) declare
293 // jpeg_mem_src with non-const buffer parameter. Use const_cast for compatibility.
294 // The buffer is only read, not modified, so this is safe.
295 jpeg_mem_src(&decompressor.get(),
296 const_cast<unsigned char*>(compressed_data.data()),
297 static_cast<unsigned long>(compressed_data.size()));
298
299 // Read JPEG header
300 int header_result = jpeg_read_header(&decompressor.get(), TRUE);
301 if (header_result != JPEG_HEADER_OK) {
302 return make_decompression_error("Invalid JPEG header");
303 }
304
305 // Validate dimensions if provided
306 if (params.width > 0 && decompressor->image_width != params.width) {
307 return make_decompression_error(
308 "Image width mismatch: expected " + std::to_string(params.width) +
309 ", got " + std::to_string(decompressor->image_width));
310 }
311 if (params.height > 0 && decompressor->image_height != params.height) {
312 return make_decompression_error(
313 "Image height mismatch: expected " + std::to_string(params.height) +
314 ", got " + std::to_string(decompressor->image_height));
315 }
316
317 // Request RGB output for color images
318 if (decompressor->num_components == 3) {
319 decompressor->out_color_space = JCS_RGB;
320 }
321
322 // Start decompression
323 jpeg_start_decompress(&decompressor.get());
324
325 // Allocate output buffer
326 JDIMENSION row_stride = decompressor->output_width *
327 static_cast<JDIMENSION>(decompressor->output_components);
328 size_t output_size = static_cast<size_t>(row_stride) * decompressor->output_height;
329 std::vector<uint8_t> output(output_size);
330
331 // Read scanlines
332 while (decompressor->output_scanline < decompressor->output_height) {
333 JSAMPROW row = output.data() +
334 decompressor->output_scanline * row_stride;
335 jpeg_read_scanlines(&decompressor.get(), &row, 1);
336 }
337
338 // Finish decompression
339 jpeg_finish_decompress(&decompressor.get());
340
341 // Build output parameters from JPEG header info
342 image_params output_params;
343 output_params.width = static_cast<uint16_t>(decompressor->output_width);
344 output_params.height = static_cast<uint16_t>(decompressor->output_height);
345 output_params.bits_allocated = 8;
346 output_params.bits_stored = 8;
347 output_params.high_bit = 7;
348 output_params.samples_per_pixel = static_cast<uint16_t>(decompressor->output_components);
349 output_params.planar_configuration = 0; // Interleaved
350 output_params.pixel_representation = 0; // Unsigned
351
352 if (output_params.samples_per_pixel == 1) {
354 } else {
356 }
357
358 return make_compression_ok(std::move(output), output_params);
359#endif // PACS_WITH_JPEG_CODEC
360 }
361};
362
363// jpeg_baseline_codec implementation
364
366 : impl_(std::make_unique<impl>()) {}
367
369
371
372jpeg_baseline_codec& jpeg_baseline_codec::operator=(jpeg_baseline_codec&&) noexcept = default;
373
374std::string_view jpeg_baseline_codec::transfer_syntax_uid() const noexcept {
375 return kTransferSyntaxUID;
376}
377
378std::string_view jpeg_baseline_codec::name() const noexcept {
379 return "JPEG Baseline (Process 1)";
380}
381
382bool jpeg_baseline_codec::is_lossy() const noexcept {
383 return true;
384}
385
386bool jpeg_baseline_codec::can_encode(const image_params& params) const noexcept {
387 return params.valid_for_jpeg_baseline();
388}
389
390bool jpeg_baseline_codec::can_decode(const image_params& params) const noexcept {
391 // Can decode any 8-bit grayscale or color JPEG
392 // Width/height can be 0 (unknown) for decode - will read from JPEG header
393 if (params.bits_allocated != 0 && params.bits_allocated != 8) return false;
394 if (params.samples_per_pixel != 0 &&
395 params.samples_per_pixel != 1 &&
396 params.samples_per_pixel != 3) {
397 return false;
398 }
399 return true;
400}
401
403 std::span<const uint8_t> pixel_data,
404 const image_params& params,
405 const compression_options& options) const {
406 return impl_->encode(pixel_data, params, options);
407}
408
410 std::span<const uint8_t> compressed_data,
411 const image_params& params) const {
412 return impl_->decode(compressed_data, params);
413}
414
415} // 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 Baseline (Process 1) codec implementation.
bool is_lossy() const noexcept override
Checks if this codec produces lossy compression.
bool can_decode(const image_params &params) const noexcept override
Checks if this codec can decode data with given parameters.
std::string_view name() const noexcept override
Returns a human-readable name for the codec.
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 Baseline format.
codec_result decode(std::span< const uint8_t > compressed_data, const image_params &params) const override
Decompresses JPEG Baseline data.
jpeg_baseline_codec()
Constructs a JPEG Baseline codec instance.
bool can_encode(const image_params &params) const noexcept override
Checks if this codec supports the given image parameters.
@ error
Node returned an error.
@ monochrome2
Minimum pixel value displayed as black.
constexpr int compression_error
Definition result.h:78
constexpr int decompression_error
Definition result.h:79
@ get
C-GET retrieve request/response.
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.
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.
bool valid_for_jpeg_baseline() const noexcept
Validates image parameters for JPEG Baseline compression.
bool is_grayscale() const noexcept
Checks if the image is grayscale (single sample per pixel).
uint16_t high_bit
High bit position (0028,0102) Typically bits_stored - 1.