Rendering Engine 0.2.0
Modular Graphics Rendering Engine | v0.2.0
Loading...
Searching...
No Matches
image_codec_jpeg.hpp
Go to the documentation of this file.
1// This file is part of the Rendering Engine project.
2// Author: Alexander Obzherin <alexanderobzherin@gmail.com>
3// Copyright (c) 2025 Alexander Obzherin
4// Distributed under the terms of the zlib License. See LICENSE.md for details.
5
6/**
7 * @file image_codec_jpeg.hpp
8 * @brief JPEG read/write backend using libjpeg.
9 *
10 * Provides low-level I/O routines bridging `ImageData` and the libjpeg API.
11 * Supports 8-bit RGB images for loading and saving.
12 *
13 * @details
14 * The implementation follows the official libjpeg API workflow:
15 * - https://libjpeg.sourceforge.io/
16 * - Steps: setup error handler -> create compressor/decompressor -> set parameters -> read/write scanlines -> clean up.
17 *
18 * Custom error handling is implemented via `codec_error_mgr` to safely recover
19 * from libjpeg�s internal `longjmp` behavior.
20 *
21 * @note This file is an internal backend of the Rendering Engine and is not part of the public API.
22 *
23 * @see rendering_engine::ImageData
24 */
25#pragma once
26
27#include <iostream>
28#include <stdio.h>
29#include "jpeglib.h"
30#include <setjmp.h>
31#include "image_data.hpp"
32
33 /**
34 * @brief Saves image data to a JPEG file.
35 * @param imageData Source image in RGB format.
36 * @param filename Path to the output file.
37 */
38static void SaveTextureFileJpeg(rendering_engine::ImageData const& imageData, char const* filename)
39{
40 auto const imageDataVector = imageData.GetImageDataRGB();
41 JSAMPLE* imageBuffer = new JSAMPLE[imageDataVector.size()];
42 std::copy(imageDataVector.begin(), imageDataVector.end(), imageBuffer);
43 int quality = 100;
44
45 struct jpeg_compress_struct cinfo;
46 struct jpeg_error_mgr jerr;
47
48 FILE* outfile; /* target file */
49 JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
50 int row_stride; /* physical row width in image buffer */
51
52 cinfo.err = jpeg_std_error(&jerr);
53 jpeg_create_compress(&cinfo);
54
55 if( (outfile = fopen(filename, "wb")) == NULL ) {
56 fprintf(stderr, "can't open %s\n", filename);
57 exit(1);
58 }
59 jpeg_stdio_dest(&cinfo, outfile);
60
61 cinfo.image_width = imageData.GetWidth(); /* image width and height, in pixels */
62 cinfo.image_height = imageData.GetHeight();
63 cinfo.input_components = 3; /* # of color components per pixel */
64 cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
65
66 jpeg_set_defaults(&cinfo);
67
68 jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);
69
70 jpeg_start_compress(&cinfo, TRUE);
71
72 row_stride = imageData.GetWidth() * 3; /* JSAMPLEs per row in image_buffer */
73
74 while( cinfo.next_scanline < cinfo.image_height )
75 {
76 /* jpeg_write_scanlines expects an array of pointers to scanlines.
77 * Here the array is only one element long, but you could pass
78 * more than one scanline at a time if that's more convenient.
79 */
80 row_pointer[0] = &imageBuffer[cinfo.next_scanline * row_stride];
81 (void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
82 }
83
84 jpeg_finish_compress(&cinfo);
85 /* After finish_compress, we can close the output file. */
86 fclose(outfile);
87
88 jpeg_destroy_compress(&cinfo);
89 delete[] imageBuffer;
90}
91
93 struct jpeg_error_mgr pub; /* "public" fields */
94
95 jmp_buf setjmp_buffer; /* for return to caller */
96};
97
99
100/*
101 * Here's the routine that will replace the standard error_exit method:
102 */
103
104METHODDEF(void)
105codec_error_exit(j_common_ptr cinfo)
106{
107 /* cinfo->err really points to a codec_error_mgr struct, so coerce pointer */
108 codec_error_mgr_ptr codecErr = (codec_error_mgr_ptr)cinfo->err;
109
110 /* Always display the message. */
111 /* We could postpone this until after returning, if we chose. */
112 (*cinfo->err->output_message) (cinfo);
113
114 /* Return control to the setjmp point */
115 longjmp(codecErr->setjmp_buffer, 1);
116}
117
118static bool
119DoReadJpegFile(struct jpeg_decompress_struct* cinfo,
120 char const* filename, unsigned int& width, unsigned int& height, std::vector<unsigned int>& rgbImageDataVector);
121
122/*
123 * Sample routine for JPEG decompression. We assume that the source file name
124 * is passed in. We want to return 1 on success, 0 on error.
125 */
126
127 /**
128 * @brief Reads a JPEG file into an RGB image buffer.
129 * @param filename Path to input JPEG file.
130 * @param width [out] Image width in pixels.
131 * @param height [out] Image height in pixels.
132 * @param rgbImageDataVector [out] Output vector of RGB pixel data (3 bytes per pixel).
133 * @return true on success, false on failure.
134 */
135static bool ReadJpegFile(char const* filename, unsigned int& width, unsigned int& height, std::vector<unsigned int>& rgbImageDataVector)
136{
137 /* This struct contains the JPEG decompression parameters and pointers to
138 * working space (which is allocated as needed by the JPEG library).
139 */
140 struct jpeg_decompress_struct cinfo;
141
142 return DoReadJpegFile(&cinfo, filename, width, height, rgbImageDataVector);
143}
144
145/*
146 * We call the libjpeg API from within a separate function, because modifying
147 * the local non-volatile jpeg_decompress_struct instance below the setjmp()
148 * return point and then accessing the instance after setjmp() returns would
149 * result in undefined behavior that may potentially overwrite all or part of
150 * the structure.
151 */
152
153bool
154DoReadJpegFile(struct jpeg_decompress_struct* cinfo, char const* filename, unsigned int& width, unsigned int& height, std::vector<unsigned int>& rgbImageDataVector)
155{
156 /* We use our private extension JPEG error handler.
157 * Note that this struct must live as long as the main JPEG parameter
158 * struct, to avoid dangling-pointer problems.
159 */
160 struct codec_error_mgr jerr;
161
162 FILE* infile; /* source file */
163 JSAMPARRAY buffer; /* Output row buffer */
164 int row_stride; /* physical row width in output buffer */
165
166 if( (infile = fopen(filename, "rb")) == NULL )
167 {
168 std::cout << "File can not be open." << std::endl;
169 return false;
170 }
171
172 /* Step 1: allocate and initialize JPEG decompression object */
173
174 /* We set up the normal JPEG error routines, then override error_exit. */
175 cinfo->err = jpeg_std_error(&jerr.pub);
176 jerr.pub.error_exit = codec_error_exit;
177 /* Establish the setjmp return context for my_error_exit to use. */
178 if( setjmp(jerr.setjmp_buffer) ) {
179 /* If we get here, the JPEG code has signaled an error.
180 * We need to clean up the JPEG object, close the input file, and return.
181 */
182 jpeg_destroy_decompress(cinfo);
183 fclose(infile);
184 return false;
185 }
186 /* Now we can initialize the JPEG decompression object. */
187 jpeg_create_decompress(cinfo);
188
189 /* Step 2: specify data source (eg, a file) */
190
191 jpeg_stdio_src(cinfo, infile);
192
193 /* Step 3: read file parameters with jpeg_read_header() */
194
195 (void)jpeg_read_header(cinfo, TRUE);
196 /* We can ignore the return value from jpeg_read_header since
197 * (a) suspension is not possible with the stdio data source, and
198 * (b) we passed TRUE to reject a tables-only JPEG file as an error.
199 * See libjpeg.txt for more info.
200 */
201
202 /* Step 4: set parameters for decompression */
203
204 /* In this example, we don't need to change any of the defaults set by
205 * jpeg_read_header(), so we do nothing here.
206 */
207
208 /* Step 5: Start decompressor and fulfill the output data */
209 width = cinfo->image_width;
210 height = cinfo->image_height;
211
212 rgbImageDataVector.clear();
213
214 (void)jpeg_start_decompress(cinfo);
215 /* We can ignore the return value since suspension is not possible
216 * with the stdio data source.
217 */
218
219 /* We may need to do some setup of our own at this point before reading
220 * the data. After jpeg_start_decompress() we have the correct scaled
221 * output image dimensions available, as well as the output colormap
222 * if we asked for color quantization.
223 * In this example, we need to make an output work buffer of the right size.
224 */
225 /* JSAMPLEs per row in output buffer */
226 row_stride = cinfo->output_width * cinfo->output_components;
227 /* Make a one-row-high sample array that will go away when done with image */
228 buffer = (*cinfo->mem->alloc_sarray)
229 ((j_common_ptr)cinfo, JPOOL_IMAGE, row_stride, 1);
230
231 /* Step 6: while (scan lines remain to be read) */
232 /* jpeg_read_scanlines(...); */
233
234 /* Here we use the library's state variable cinfo->output_scanline as the
235 * loop counter, so that we don't have to keep track ourselves.
236 */
237 ;
238 while( cinfo->output_scanline < cinfo->output_height ) {
239 /* jpeg_read_scanlines expects an array of pointers to scanlines.
240 * Here the array is only one element long, but you could ask for
241 * more than one scanline at a time if that's more convenient.
242 */
243 (void)jpeg_read_scanlines(cinfo, buffer, 1);
244 /* Assume put_scanline_someplace wants a pointer and sample count. */
245 for( int i = 0; i < row_stride; ++i )
246 {
247 rgbImageDataVector.push_back(*(buffer[0] + i));
248 }
249 }
250
251 /* Step 7: Finish decompression */
252
253 (void)jpeg_finish_decompress(cinfo);
254 /* We can ignore the return value since suspension is not possible
255 * with the stdio data source.
256 */
257
258 /* Step 8: Release JPEG decompression object */
259
260 /* This is an important step since it will release a good deal of memory. */
261 jpeg_destroy_decompress(cinfo);
262
263 /* After finish_decompress, we can close the input file.
264 * Here we postpone it until after no more JPEG errors are possible,
265 * so as to simplify the setjmp error logic above. (Actually, I don't
266 * think that jpeg_destroy can do an error exit, but why assume anything...)
267 */
268 fclose(infile);
269
270 /* At this point you may want to check to see whether any corrupt-data
271 * warnings occurred (test whether jerr.pub.num_warnings is nonzero).
272 */
273
274 /* And we're done! */
275 return true;
276}
277
278/**
279 * @brief Decode a JPEG image directly from a memory buffer.
280 *
281 * This function behaves similarly to ReadJpegFile(), but instead of
282 * reading from disk, it reads raw JPEG bytes already loaded in memory.
283 *
284 * Useful when assets are stored in:
285 * - packed archives (Pack.bin)
286 * - networked streams
287 * - compressed inline resources
288 *
289 * The function expects the buffer to contain valid JPEG-formatted data.
290 *
291 * @param memory
292 * Pointer to the start of the JPEG byte stream.
293 *
294 * @param memorySize
295 * Total size of the memory buffer in bytes.
296 *
297 * @param width [out]
298 * Decoded image width in pixels.
299 *
300 * @param height [out]
301 * Decoded image height in pixels.
302 *
303 * @param rgbImageDataVector [out]
304 * Output RGB pixel buffer (3 bytes per pixel).
305 *
306 * @return
307 * - `true` → Successful decode.
308 * - `false` → Memory buffer was not a valid JPEG stream or decode failed.
309 *
310 * @warning
311 * The function does not attempt format detection beyond JPEG signature.
312 * Callers must ensure the memory buffer contains JPEG data.
313 */
315 const unsigned char* memory,
316 size_t memorySize,
317 unsigned int& width,
318 unsigned int& height,
319 std::vector<unsigned int>& rgbImageDataVector)
320{
321 struct jpeg_decompress_struct cinfo;
322 struct codec_error_mgr jerr;
323
324 cinfo.err = jpeg_std_error(&jerr.pub);
325 jerr.pub.error_exit = codec_error_exit;
326
327 if (setjmp(jerr.setjmp_buffer)) {
328 jpeg_destroy_decompress(&cinfo);
329 return false;
330 }
331
332 jpeg_create_decompress(&cinfo);
333
334 jpeg_mem_src(&cinfo, memory, memorySize);
335
336 jpeg_read_header(&cinfo, TRUE);
337 jpeg_start_decompress(&cinfo);
338
339 width = cinfo.output_width;
340 height = cinfo.output_height;
341 unsigned int components = cinfo.output_components; // should = 3
342
343 size_t row_stride = width * components;
344 rgbImageDataVector.resize(width * height * components);
345
346 while (cinfo.output_scanline < cinfo.output_height)
347 {
348 unsigned char* row = (unsigned char*)(
349 rgbImageDataVector.data() +
350 cinfo.output_scanline * row_stride
351 );
352 jpeg_read_scanlines(&cinfo, &row, 1);
353 }
354
355 jpeg_finish_decompress(&cinfo);
356 jpeg_destroy_decompress(&cinfo);
357
358 return true;
359}
Represents raw 2D image data stored in memory.
unsigned int GetWidth() const
Returns the image width.
std::vector< uint8_t > GetImageDataRGB() const
Gets raw image data in RGB format.
unsigned int GetHeight() const
Returns the image height.
static bool ReadJpegFile(char const *filename, unsigned int &width, unsigned int &height, std::vector< unsigned int > &rgbImageDataVector)
Reads a JPEG file into an RGB image buffer.
struct codec_error_mgr * codec_error_mgr_ptr
codec_error_exit(j_common_ptr cinfo)
static bool DoReadJpegFile(struct jpeg_decompress_struct *cinfo, char const *filename, unsigned int &width, unsigned int &height, std::vector< unsigned int > &rgbImageDataVector)
static bool ReadJpegFromMemory(const unsigned char *memory, size_t memorySize, unsigned int &width, unsigned int &height, std::vector< unsigned int > &rgbImageDataVector)
Decode a JPEG image directly from a memory buffer.
static void SaveTextureFileJpeg(rendering_engine::ImageData const &imageData, char const *filename)
Saves image data to a JPEG file.
struct jpeg_error_mgr pub