scimesh 0.3.2
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
image.cpp
Go to the documentation of this file.
1#include <scimesh/image.h>
2#include <fstream>
3#include <cstring>
4#include <algorithm>
5#include <cmath>
6
7// When building standalone examples (not the test suite), define the stb
8// implementation here. The test suite provides its own definition in
9// test_primitives.cpp, so guard against double-definition at link time.
10// STBI_STATIC makes all stb symbols file-local, preventing linker clashes
11// when scimesh is linked alongside other libraries that also use stb_image.
12//
13// Override stb_image's default assert() behaviour: the library already
14// handles errors gracefully (returns NULL / 0), so converting asserts to
15// no-ops is safe and prevents process termination (CRAN policy).
16#define STBI_ASSERT(x) ((void)0)
17#define STBIW_ASSERT(x) ((void)0)
18
19#ifdef SCIMESH_STB_WRITE_IMPL
20#ifndef STB_IMAGE_WRITE_IMPLEMENTATION
21#define STBI_STATIC
22#define STB_IMAGE_WRITE_IMPLEMENTATION
23#endif
24#endif
25#include "stb_image_write.h"
26
27#ifdef SCIMESH_STB_READ_IMPL
28#ifndef STB_IMAGE_IMPLEMENTATION
29#define STBI_STATIC
30#define STB_IMAGE_IMPLEMENTATION
31#endif
32#endif
33#include "stb_image.h"
34
35namespace scimesh {
36
37Image::Image(int w, int h) : width(w), height(h), pixels(w * h * 4, 0) {}
38
39void Image::set_pixel(int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
40 if (x < 0 || x >= width || y < 0 || y >= height)
41 return;
42 int idx = (y * width + x) * 4;
43 pixels[idx] = r;
44 pixels[idx + 1] = g;
45 pixels[idx + 2] = b;
46 pixels[idx + 3] = a;
47}
48
49void Image::get_pixel(int x, int y, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) const {
50 if (x < 0 || x >= width || y < 0 || y >= height) {
51 r = g = b = a = 0;
52 return;
53 }
54 int idx = (y * width + x) * 4;
55 r = pixels[idx];
56 g = pixels[idx + 1];
57 b = pixels[idx + 2];
58 a = pixels[idx + 3];
59}
60
61void Image::clear(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
62 for (int i = 0; i < width * height; ++i) {
63 pixels[i * 4] = r;
64 pixels[i * 4 + 1] = g;
65 pixels[i * 4 + 2] = b;
66 pixels[i * 4 + 3] = a;
67 }
68}
69
70void Image::clear_float(float r, float g, float b, float a) {
71 clear(static_cast<uint8_t>(std::clamp(r, 0.0f, 1.0f) * 255.0f),
72 static_cast<uint8_t>(std::clamp(g, 0.0f, 1.0f) * 255.0f),
73 static_cast<uint8_t>(std::clamp(b, 0.0f, 1.0f) * 255.0f),
74 static_cast<uint8_t>(std::clamp(a, 0.0f, 1.0f) * 255.0f));
75}
76
77Image Image::downsample_box(int factor) const {
78 if (factor <= 1)
79 return *this;
80
81 int out_w = width / factor;
82 int out_h = height / factor;
83 Image result(out_w, out_h);
84
85 int n = factor * factor;
86 for (int y = 0; y < out_h; y++) {
87 for (int x = 0; x < out_w; x++) {
88 int r_sum = 0, g_sum = 0, b_sum = 0, a_sum = 0;
89 for (int dy = 0; dy < factor; dy++) {
90 for (int dx = 0; dx < factor; dx++) {
91 int sx = x * factor + dx;
92 int sy = y * factor + dy;
93 int idx = (sy * width + sx) * 4;
94 r_sum += pixels[idx];
95 g_sum += pixels[idx + 1];
96 b_sum += pixels[idx + 2];
97 a_sum += pixels[idx + 3];
98 }
99 }
100 result.set_pixel(x, y,
101 static_cast<uint8_t>(r_sum / n),
102 static_cast<uint8_t>(g_sum / n),
103 static_cast<uint8_t>(b_sum / n),
104 static_cast<uint8_t>(a_sum / n));
105 }
106 }
107 return result;
108}
109
110void Image::crop(int x, int y, int w, int h) {
111 if (w <= 0 || h <= 0) {
112 width = height = 0;
113 pixels.clear();
114 return;
115 }
116 int x0 = std::max(0, x);
117 int y0 = std::max(0, y);
118 int x1 = std::min(width, x + w);
119 int y1 = std::min(height, y + h);
120 int new_w = std::max(0, x1 - x0);
121 int new_h = std::max(0, y1 - y0);
122
123 if (new_w == 0 || new_h == 0) {
124 width = height = 0;
125 pixels.clear();
126 return;
127 }
128
129 std::vector<uint8_t> new_pixels(new_w * new_h * 4);
130 for (int cy = 0; cy < new_h; ++cy) {
131 int src_y = y0 + cy;
132 int src_offset = (src_y * width + x0) * 4;
133 int dst_offset = cy * new_w * 4;
134 std::memcpy(new_pixels.data() + dst_offset,
135 pixels.data() + src_offset,
136 new_w * 4);
137 }
138 width = new_w;
139 height = new_h;
140 pixels = std::move(new_pixels);
141}
142
143void Image::merge(const Image &other, MergeDirection direction) {
144 if (other.width <= 0 || other.height <= 0) return;
145
146 int new_w = 0, new_h = 0;
147 int this_off_x = 0, this_off_y = 0;
148 int other_off_x = 0, other_off_y = 0;
149
150 switch (direction) {
153 if (height != other.height) return;
154 new_w = width + other.width;
155 new_h = height;
156 if (direction == MergeDirection::LEFT) {
157 other_off_x = 0;
158 this_off_x = other.width;
159 } else {
160 this_off_x = 0;
161 other_off_x = width;
162 }
163 break;
166 if (width != other.width) return;
167 new_w = width;
168 new_h = height + other.height;
169 if (direction == MergeDirection::TOP) {
170 other_off_y = 0;
171 this_off_y = other.height;
172 } else {
173 this_off_y = 0;
174 other_off_y = height;
175 }
176 break;
177 }
178
179 std::vector<uint8_t> new_pixels(new_w * new_h * 4, 0);
180
181 for (int y = 0; y < height; ++y) {
182 std::memcpy(new_pixels.data() + ((y + this_off_y) * new_w + this_off_x) * 4,
183 pixels.data() + y * width * 4,
184 width * 4);
185 }
186 for (int y = 0; y < other.height; ++y) {
187 std::memcpy(new_pixels.data() + ((y + other_off_y) * new_w + other_off_x) * 4,
188 other.pixels.data() + y * other.width * 4,
189 other.width * 4);
190 }
191
192 width = new_w;
193 height = new_h;
194 pixels = std::move(new_pixels);
195}
196
197void Image::grow(int top, int bottom, int left, int right, const Color &background) {
198 if (top < 0 || bottom < 0 || left < 0 || right < 0) return;
199
200 int new_w = width + left + right;
201 int new_h = height + top + bottom;
202 if (new_w <= 0 || new_h <= 0) return;
203
204 uint8_t br = static_cast<uint8_t>(std::clamp(background.r, 0.0f, 1.0f) * 255.0f);
205 uint8_t bg = static_cast<uint8_t>(std::clamp(background.g, 0.0f, 1.0f) * 255.0f);
206 uint8_t bb = static_cast<uint8_t>(std::clamp(background.b, 0.0f, 1.0f) * 255.0f);
207 uint8_t ba = static_cast<uint8_t>(std::clamp(background.a, 0.0f, 1.0f) * 255.0f);
208
209 std::vector<uint8_t> new_pixels(new_w * new_h * 4);
210 for (int i = 0; i < new_w * new_h; ++i) {
211 new_pixels[i * 4] = br;
212 new_pixels[i * 4 + 1] = bg;
213 new_pixels[i * 4 + 2] = bb;
214 new_pixels[i * 4 + 3] = ba;
215 }
216
217 for (int y = 0; y < height; ++y) {
218 std::memcpy(new_pixels.data() + ((y + top) * new_w + left) * 4,
219 pixels.data() + y * width * 4,
220 width * 4);
221 }
222
223 width = new_w;
224 height = new_h;
225 pixels = std::move(new_pixels);
226}
227
228void Image::rotate_90(bool clockwise) {
229 int new_w = height;
230 int new_h = width;
231 std::vector<uint8_t> new_pixels(new_w * new_h * 4);
232 for (int y = 0; y < height; ++y) {
233 for (int x = 0; x < width; ++x) {
234 int dst_x, dst_y;
235 if (clockwise) {
236 dst_x = height - 1 - y;
237 dst_y = x;
238 } else {
239 dst_x = y;
240 dst_y = width - 1 - x;
241 }
242 std::memcpy(new_pixels.data() + (dst_y * new_w + dst_x) * 4,
243 pixels.data() + (y * width + x) * 4, 4);
244 }
245 }
246 width = new_w;
247 height = new_h;
248 pixels = std::move(new_pixels);
249}
250
251void Image::scale(int new_width, int new_height) {
252 if (new_width <= 0 || new_height <= 0) {
253 width = height = 0;
254 pixels.clear();
255 return;
256 }
257 if (new_width == width && new_height == height) return;
258
259 std::vector<uint8_t> new_pixels(new_width * new_height * 4);
260 for (int dy = 0; dy < new_height; ++dy) {
261 int src_y = dy * height / new_height;
262 for (int dx = 0; dx < new_width; ++dx) {
263 int src_x = dx * width / new_width;
264 std::memcpy(new_pixels.data() + (dy * new_width + dx) * 4,
265 pixels.data() + (src_y * width + src_x) * 4, 4);
266 }
267 }
268 width = new_width;
269 height = new_height;
270 pixels = std::move(new_pixels);
271}
272
273void Image::crop_to_content(CropContentDirection direction, const Color &background) {
274 if (width <= 0 || height <= 0) return;
275
276 uint8_t br = static_cast<uint8_t>(std::clamp(background.r, 0.0f, 1.0f) * 255.0f);
277 uint8_t bg = static_cast<uint8_t>(std::clamp(background.g, 0.0f, 1.0f) * 255.0f);
278 uint8_t bb = static_cast<uint8_t>(std::clamp(background.b, 0.0f, 1.0f) * 255.0f);
279 uint8_t ba = static_cast<uint8_t>(std::clamp(background.a, 0.0f, 1.0f) * 255.0f);
280
281 auto is_bg = [&](int x, int y) {
282 int idx = (y * width + x) * 4;
283 return pixels[idx] == br && pixels[idx+1] == bg &&
284 pixels[idx+2] == bb && pixels[idx+3] == ba;
285 };
286
287 int crop_left = 0, crop_right = 0, crop_top = 0, crop_bottom = 0;
288
289 bool do_left = (direction == CropContentDirection::LEFT ||
291 direction == CropContentDirection::ALL);
292 bool do_right = (direction == CropContentDirection::RIGHT ||
294 direction == CropContentDirection::ALL);
295 bool do_top = (direction == CropContentDirection::TOP ||
296 direction == CropContentDirection::VERTICAL ||
297 direction == CropContentDirection::ALL);
298 bool do_bottom = (direction == CropContentDirection::BOTTOM ||
299 direction == CropContentDirection::VERTICAL ||
300 direction == CropContentDirection::ALL);
301
302 if (do_left) {
303 for (int x = 0; x < width; ++x) {
304 bool all_bg = true;
305 for (int y = 0; y < height; ++y) {
306 if (!is_bg(x, y)) { all_bg = false; break; }
307 }
308 if (!all_bg) break;
309 crop_left = x + 1;
310 }
311 }
312
313 if (do_right) {
314 for (int x = width - 1; x >= crop_left; --x) {
315 bool all_bg = true;
316 for (int y = 0; y < height; ++y) {
317 if (!is_bg(x, y)) { all_bg = false; break; }
318 }
319 if (!all_bg) break;
320 crop_right = width - x;
321 }
322 }
323
324 if (do_top) {
325 for (int y = 0; y < height; ++y) {
326 bool all_bg = true;
327 for (int x = 0; x < width; ++x) {
328 if (!is_bg(x, y)) { all_bg = false; break; }
329 }
330 if (!all_bg) break;
331 crop_top = y + 1;
332 }
333 }
334
335 if (do_bottom) {
336 for (int y = height - 1; y >= crop_top; --y) {
337 bool all_bg = true;
338 for (int x = 0; x < width; ++x) {
339 if (!is_bg(x, y)) { all_bg = false; break; }
340 }
341 if (!all_bg) break;
342 crop_bottom = height - y;
343 }
344 }
345
346 int new_w = width - crop_left - crop_right;
347 int new_h = height - crop_top - crop_bottom;
348 crop(crop_left, crop_top, std::max(0, new_w), std::max(0, new_h));
349}
350
351Color Image::sample_bilinear(float u, float v) const {
352 if (width < 1 || height < 1) return Color();
353 u = std::max(0.0f, std::min(1.0f, u));
354 v = std::max(0.0f, std::min(1.0f, v));
355 float fx = u * (width - 1);
356 float fy = v * (height - 1);
357 int x0 = static_cast<int>(fx);
358 int y0 = static_cast<int>(fy);
359 int x1 = std::min(x0 + 1, width - 1);
360 int y1 = std::min(y0 + 1, height - 1);
361 float sx = fx - x0;
362 float sy = fy - y0;
363
364 auto get = [this](int px, int py) -> Color {
365 int idx = (py * width + px) * 4;
366 return Color(pixels[idx] / 255.0f, pixels[idx+1] / 255.0f,
367 pixels[idx+2] / 255.0f, pixels[idx+3] / 255.0f);
368 };
369 Color c00 = get(x0, y0); Color c10 = get(x1, y0);
370 Color c01 = get(x0, y1); Color c11 = get(x1, y1);
371
372 return Color(
373 c00.r * (1-sx)*(1-sy) + c10.r * sx*(1-sy) + c01.r * (1-sx)*sy + c11.r * sx*sy,
374 c00.g * (1-sx)*(1-sy) + c10.g * sx*(1-sy) + c01.g * (1-sx)*sy + c11.g * sx*sy,
375 c00.b * (1-sx)*(1-sy) + c10.b * sx*(1-sy) + c01.b * (1-sx)*sy + c11.b * sx*sy,
376 c00.a * (1-sx)*(1-sy) + c10.a * sx*(1-sy) + c01.a * (1-sx)*sy + c11.a * sx*sy);
377}
378
379void Image::apply_contrast(float contrast) {
380 if (contrast == 1.0f) return;
381 for (int i = 0; i < width * height; ++i) {
382 int idx = i * 4;
383 float r = pixels[idx] / 255.0f;
384 float g = pixels[idx + 1] / 255.0f;
385 float b = pixels[idx + 2] / 255.0f;
386 pixels[idx] = static_cast<uint8_t>(std::clamp((r - 0.5f) * contrast + 0.5f, 0.0f, 1.0f) * 255.0f);
387 pixels[idx + 1] = static_cast<uint8_t>(std::clamp((g - 0.5f) * contrast + 0.5f, 0.0f, 1.0f) * 255.0f);
388 pixels[idx + 2] = static_cast<uint8_t>(std::clamp((b - 0.5f) * contrast + 0.5f, 0.0f, 1.0f) * 255.0f);
389 }
390}
391
392bool Image::write_ppm(const std::string &filename) const {
393 std::ofstream f(filename, std::ios::binary);
394 if (!f)
395 return false;
396 f << "P6\n" << width << " " << height << "\n255\n";
397 for (int i = 0; i < width * height; ++i) {
398 f.put(pixels[i * 4]);
399 f.put(pixels[i * 4 + 1]);
400 f.put(pixels[i * 4 + 2]);
401 }
402 return f.good();
403}
404
405bool Image::write_bmp(const std::string &filename) const {
406 // BMP with alpha (BGRA, bottom-up rows)
407 int row_size = width * 4;
408 int pixel_data_size = row_size * height;
409 int file_size = 14 + 40 + 56 + pixel_data_size; // BITMAPV4 header
410
411 std::ofstream f(filename, std::ios::binary);
412 if (!f)
413 return false;
414
415 // BMP file header (14 bytes)
416 uint8_t fh[14] = {0};
417 fh[0] = 'B';
418 fh[1] = 'M';
419 std::memcpy(fh + 2, &file_size, 4);
420 // reserved = 0
421 uint32_t pixel_offset = 14 + 40 + 56;
422 std::memcpy(fh + 10, &pixel_offset, 4);
423 f.write(reinterpret_cast<const char *>(fh), 14);
424
425 // BITMAPV4HEADER (108 bytes, but we use 40 + 56 = 96... actually let's use V4 = 108)
426 // For simplicity, use BITMAPINFOHEADER (40 bytes) + bit masks (16 bytes) = 56 byte header
427 uint32_t header_size = 56;
428 f.write(reinterpret_cast<const char *>(&header_size), 4);
429 int32_t w = width;
430 int32_t h = height;
431 f.write(reinterpret_cast<const char *>(&w), 4);
432 f.write(reinterpret_cast<const char *>(&h), 4);
433 uint16_t planes = 1;
434 f.write(reinterpret_cast<const char *>(&planes), 2);
435 uint16_t bpp = 32;
436 f.write(reinterpret_cast<const char *>(&bpp), 2);
437 uint32_t compression = 3; // BI_BITFIELDS
438 f.write(reinterpret_cast<const char *>(&compression), 4);
439 uint32_t img_size = pixel_data_size;
440 f.write(reinterpret_cast<const char *>(&img_size), 4);
441 int32_t ppm = 2835;
442 f.write(reinterpret_cast<const char *>(&ppm), 4);
443 f.write(reinterpret_cast<const char *>(&ppm), 4);
444 uint32_t colors_used = 0;
445 f.write(reinterpret_cast<const char *>(&colors_used), 4);
446 uint32_t colors_important = 0;
447 f.write(reinterpret_cast<const char *>(&colors_important), 4);
448 // Color masks (16 bytes): RGBA
449 uint32_t r_mask = 0x00FF0000;
450 uint32_t g_mask = 0x0000FF00;
451 uint32_t b_mask = 0x000000FF;
452 uint32_t a_mask = 0xFF000000;
453 f.write(reinterpret_cast<const char *>(&r_mask), 4);
454 f.write(reinterpret_cast<const char *>(&g_mask), 4);
455 f.write(reinterpret_cast<const char *>(&b_mask), 4);
456 f.write(reinterpret_cast<const char *>(&a_mask), 4);
457
458 // Pixel data (bottom-up, BGRA)
459 for (int y = height - 1; y >= 0; --y) {
460 for (int x = 0; x < width; ++x) {
461 int idx = (y * width + x) * 4;
462 f.put(pixels[idx + 2]); // B
463 f.put(pixels[idx + 1]); // G
464 f.put(pixels[idx]); // R
465 f.put(pixels[idx + 3]); // A
466 }
467 }
468 return f.good();
469}
470
471bool Image::write_tga(const std::string &filename, bool use24bit) const {
472 if (width <= 0 || height <= 0 || pixels.empty())
473 return false;
474
475 const int bytes_per_pixel = use24bit ? 3 : 4;
476
477 std::ofstream f(filename, std::ios::binary);
478 if (!f)
479 return false;
480
481 // 18-byte TGA 2.0 header. Fields not set explicitly below are zero:
482 // [0] ID length (0) [1] color map type (0 = none)
483 // [2] image type (2 = uncompressed true-color)
484 // [3..7] color map spec (all 0) [8..11] x/y origin (0)
485 // [12..13] width (LE) [14..15] height (LE)
486 // [16] pixel depth [17] image descriptor
487 uint8_t header[18] = {0};
488 header[2] = 2; // uncompressed true-color
489 header[12] = static_cast<uint8_t>(width & 0xFF);
490 header[13] = static_cast<uint8_t>((width >> 8) & 0xFF);
491 header[14] = static_cast<uint8_t>(height & 0xFF);
492 header[15] = static_cast<uint8_t>((height >> 8) & 0xFF);
493 header[16] = static_cast<uint8_t>(use24bit ? 24 : 32);
494 header[17] = 0x20; // top-left origin
495 if (!use24bit) header[17] |= 0x08; // 8 attribute (alpha) bits
496
497 f.write(reinterpret_cast<const char *>(header), sizeof(header));
498 if (!f)
499 return false;
500
501 // Pixel data: TGA stores BGR(A) order, our buffer is RGBA. Swap R/B.
502 // The renderer stores row 0 as the top row, matching TGA top-left origin.
503 std::vector<uint8_t> row(static_cast<size_t>(width) * bytes_per_pixel);
504 for (int y = 0; y < height; ++y) {
505 size_t o = 0;
506 for (int x = 0; x < width; ++x) {
507 int idx = (y * width + x) * 4;
508 row[o++] = pixels[idx + 2]; // B
509 row[o++] = pixels[idx + 1]; // G
510 row[o++] = pixels[idx]; // R
511 if (!use24bit) row[o++] = pixels[idx + 3]; // A (32-bit only)
512 }
513 f.write(reinterpret_cast<const char *>(row.data()),
514 static_cast<std::streamsize>(row.size()));
515 if (!f)
516 return false;
517 }
518 return true;
519}
520
521bool Image::write_png(const std::string &filename) const {
522 if (width <= 0 || height <= 0 || pixels.empty())
523 return false;
524 int stride = width * 4;
525 return stbi_write_png(filename.c_str(), width, height, 4,
526 pixels.data(), stride) != 0;
527}
528
529// ---------------------------------------------------------------------------
530// File input (stb_image)
531// ---------------------------------------------------------------------------
532
533Image Image::read_image(const std::string &path) {
534 int w = 0, h = 0, n = 0;
535 unsigned char *data = stbi_load(path.c_str(), &w, &h, &n, 4);
536 if (!data) return Image();
537 Image img(w, h);
538 std::memcpy(img.pixels.data(), data, w * h * 4);
539 stbi_image_free(data);
540 return img;
541}
542
543// ---------------------------------------------------------------------------
544// Size normalization
545// ---------------------------------------------------------------------------
546
547void Image::pad_to_size(int target_w, int target_h, const Color &background) {
548 if (target_w <= width && target_h <= height) return;
549 int pad_top = (target_h > height) ? (target_h - height) / 2 : 0;
550 int pad_bottom = (target_h > height) ? target_h - height - pad_top : 0;
551 int pad_left = (target_w > width) ? (target_w - width) / 2 : 0;
552 int pad_right = (target_w > width) ? target_w - width - pad_left : 0;
553 grow(pad_top, pad_bottom, pad_left, pad_right, background);
554}
555
556// ---------------------------------------------------------------------------
557// grid_arrange
558// ---------------------------------------------------------------------------
559
560Image grid_arrange(const std::vector<Image> &images,
561 int ncol, int nrow,
562 FitMode fit_mode,
563 const Color &background) {
564 if (images.empty()) return Image();
565
566 int n = static_cast<int>(images.size());
567
568 // Resolve grid dimensions
569 if (ncol <= 0 && nrow <= 0) {
570 ncol = static_cast<int>(std::ceil(std::sqrt(static_cast<double>(n))));
571 nrow = (n + ncol - 1) / ncol;
572 } else if (ncol <= 0) {
573 ncol = (n + nrow - 1) / nrow;
574 } else if (nrow <= 0) {
575 nrow = (n + ncol - 1) / ncol;
576 }
577
578 // Determine max cell dimensions
579 int cell_w = 0, cell_h = 0;
580 for (const auto &img : images) {
581 cell_w = std::max(cell_w, img.width);
582 cell_h = std::max(cell_h, img.height);
583 }
584 if (cell_w <= 0 || cell_h <= 0) return Image();
585
586 bool is_1d_horizontal = (nrow == 1);
587 bool is_1d_vertical = (ncol == 1);
588
589 // Normalize images. For 1D layouts (single row or single column),
590 // only the cross-axis dimension needs to match — the merge direction
591 // only cares about one dimension. For 2D grids, both must match.
592 std::vector<Image> cells;
593 cells.reserve(n);
594
595 if (is_1d_horizontal) {
596 // Horizontal strip: normalize heights only, widths stay as-is.
597 for (const auto &img : images) {
598 Image c = img;
599 if (fit_mode == FitMode::SCALE) {
600 float ar = static_cast<float>(img.width) / img.height;
601 c.scale(static_cast<int>(cell_h * ar), cell_h);
602 } else {
603 c.pad_to_size(img.width, cell_h, background);
604 }
605 cells.push_back(std::move(c));
606 }
607 ncol = static_cast<int>(cells.size());
608 nrow = 1;
609 } else if (is_1d_vertical) {
610 // Vertical strip: normalize widths only, heights stay as-is.
611 for (const auto &img : images) {
612 Image c = img;
613 if (fit_mode == FitMode::SCALE) {
614 float ar = static_cast<float>(img.height) / img.width;
615 c.scale(cell_w, static_cast<int>(cell_w * ar));
616 } else {
617 c.pad_to_size(cell_w, img.height, background);
618 }
619 cells.push_back(std::move(c));
620 }
621 nrow = static_cast<int>(cells.size());
622 ncol = 1;
623 } else {
624 // 2D grid: normalize both dimensions.
625 for (const auto &img : images) {
626 Image c = img;
627 if (fit_mode == FitMode::SCALE) {
628 c.scale(cell_w, cell_h);
629 } else {
630 c.pad_to_size(cell_w, cell_h, background);
631 }
632 cells.push_back(std::move(c));
633 }
634
635 // Fill remaining slots with background
636 Image blank(cell_w, cell_h);
637 blank.clear_float(background.r, background.g, background.b, background.a);
638 while (static_cast<int>(cells.size()) < ncol * nrow) {
639 cells.push_back(blank);
640 }
641 }
642
643 // Assemble rows
644 std::vector<Image> rows;
645 rows.reserve(nrow);
646 for (int r = 0; r < nrow; ++r) {
647 Image row_img = cells[r * ncol];
648 for (int c = 1; c < ncol; ++c) {
649 row_img.merge(cells[r * ncol + c], MergeDirection::RIGHT);
650 }
651 rows.push_back(std::move(row_img));
652 }
653
654 // Assemble final image — grid row 0 is the conceptual top, image y=0
655 // is the bottom. merge(TOP) places `other` below `this`, so stacking
656 // upwards from row 0 puts row 0 at the top of the final image.
657 Image result = rows[0];
658 for (int r = 1; r < nrow; ++r) {
659 result.merge(rows[r], MergeDirection::TOP);
660 }
661 return result;
662}
663
664} // namespace scimesh
The Image — an RGBA pixel buffer with compositing and I/O operations.
CropContentDirection
Direction(s) for the crop_to_content() operation.
Definition image.h:37
@ BOTTOM
Crop bottom edge only.
@ RIGHT
Crop right edge only.
@ VERTICAL
Crop both top and bottom edges.
@ ALL
Crop all four edges.
@ TOP
Crop top edge only.
@ LEFT
Crop left edge only.
@ HORIZONTAL
Crop both left and right edges.
Image grid_arrange(const std::vector< Image > &images, int ncol, int nrow, FitMode fit_mode, const Color &background)
Arrange a list of images into a grid layout.
Definition image.cpp:560
MergeDirection
Direction for the merge() operation.
Definition image.h:25
@ BOTTOM
Attach other below.
@ RIGHT
Attach other to the right side.
@ TOP
Attach other above.
@ LEFT
Attach other to the left side.
FitMode
Strategy for normalizing images to a common cell size in grid_arrange().
Definition image.h:50
@ SCALE
Scale all images to match the largest cell dimensions.
An RGBA color with floating-point components.
Definition types.h:88
float g
Green channel, [0, 1].
Definition types.h:90
float r
Red channel, [0, 1].
Definition types.h:89
float b
Blue channel, [0, 1].
Definition types.h:91
float a
Alpha (opacity) channel, [0, 1]. 1.0 = fully opaque.
Definition types.h:92
A 2D RGBA image buffer.
Definition image.h:87
void apply_contrast(float contrast)
Apply a contrast adjustment to the image.
Definition image.cpp:379
int height
Image height in pixels.
Definition image.h:92
bool write_tga(const std::string &filename, bool use24bit=false) const
Write the image as a TGA file (Truevision Targa).
Definition image.cpp:471
bool write_ppm(const std::string &filename) const
Write the image as a PPM file (Portable Pixmap).
Definition image.cpp:392
void scale(int new_width, int new_height)
Scale (resize) the image to new dimensions in-place.
Definition image.cpp:251
int width
Image width in pixels.
Definition image.h:89
void crop(int x, int y, int w, int h)
Crop the image to a sub-rectangle.
Definition image.cpp:110
Image()=default
Construct an empty (0×0) image.
void set_pixel(int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Set a single pixel's RGBA value.
Definition image.cpp:39
std::vector< uint8_t > pixels
Raw pixel data: RGBA bytes, row-major, bottom-left origin.
Definition image.h:98
void grow(int top, int bottom, int left, int right, const Color &background)
Grow (pad) the image by adding borders.
Definition image.cpp:197
void rotate_90(bool clockwise=true)
Rotate the image by 90 degrees in-place.
Definition image.cpp:228
void clear_float(float r, float g, float b, float a)
Fill the entire image with an RGBA color (float values 0.0–1.0).
Definition image.cpp:70
void get_pixel(int x, int y, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) const
Get a single pixel's RGBA value.
Definition image.cpp:49
void merge(const Image &other, MergeDirection direction)
Merge (concatenate) another image onto this one.
Definition image.cpp:143
Image downsample_box(int factor) const
Downsample the image by a factor using box filtering.
Definition image.cpp:77
void pad_to_size(int target_w, int target_h, const Color &background)
Pad the image to a target size, centering the content.
Definition image.cpp:547
bool write_png(const std::string &filename) const
Write the image as a PNG file.
Definition image.cpp:521
void clear(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Fill the entire image with an RGBA color (byte values 0–255).
Definition image.cpp:61
void crop_to_content(CropContentDirection direction, const Color &background)
Crop away uniform borders of a given background color.
Definition image.cpp:273
Color sample_bilinear(float u, float v) const
Sample the image at texture coordinates (u, v) using bilinear interpolation.
Definition image.cpp:351
static Image read_image(const std::string &path)
Read an image from a file (PNG, BMP, TGA, JPEG, etc.).
Definition image.cpp:533
bool write_bmp(const std::string &filename) const
Write the image as a BMP file (Windows Bitmap).
Definition image.cpp:405