16#define STBI_ASSERT(x) ((void)0)
17#define STBIW_ASSERT(x) ((void)0)
19#ifdef SCIMESH_STB_WRITE_IMPL
20#ifndef STB_IMAGE_WRITE_IMPLEMENTATION
22#define STB_IMAGE_WRITE_IMPLEMENTATION
25#include "stb_image_write.h"
27#ifdef SCIMESH_STB_READ_IMPL
28#ifndef STB_IMAGE_IMPLEMENTATION
30#define STB_IMAGE_IMPLEMENTATION
37Image::Image(
int w,
int h) : width(w), height(h), pixels(w * h * 4, 0) {}
42 int idx = (y *
width + x) * 4;
49void Image::get_pixel(
int x,
int y, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a)
const {
54 int idx = (y *
width + x) * 4;
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));
81 int out_w =
width / factor;
82 int out_h =
height / factor;
83 Image result(out_w, out_h);
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;
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));
111 if (w <= 0 || h <= 0) {
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);
123 if (new_w == 0 || new_h == 0) {
129 std::vector<uint8_t> new_pixels(new_w * new_h * 4);
130 for (
int cy = 0; cy < new_h; ++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,
140 pixels = std::move(new_pixels);
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;
158 this_off_x = other.
width;
171 this_off_y = other.
height;
179 std::vector<uint8_t> new_pixels(new_w * new_h * 4, 0);
181 for (
int y = 0; y <
height; ++y) {
182 std::memcpy(new_pixels.data() + ((y + this_off_y) * new_w + this_off_x) * 4,
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,
194 pixels = std::move(new_pixels);
198 if (top < 0 || bottom < 0 || left < 0 || right < 0)
return;
200 int new_w =
width + left + right;
201 int new_h =
height + top + bottom;
202 if (new_w <= 0 || new_h <= 0)
return;
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);
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;
217 for (
int y = 0; y <
height; ++y) {
218 std::memcpy(new_pixels.data() + ((y + top) * new_w + left) * 4,
225 pixels = std::move(new_pixels);
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) {
240 dst_y =
width - 1 - x;
242 std::memcpy(new_pixels.data() + (dst_y * new_w + dst_x) * 4,
248 pixels = std::move(new_pixels);
252 if (new_width <= 0 || new_height <= 0) {
257 if (new_width ==
width && new_height ==
height)
return;
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,
270 pixels = std::move(new_pixels);
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);
281 auto is_bg = [&](
int x,
int y) {
282 int idx = (y *
width + x) * 4;
287 int crop_left = 0, crop_right = 0, crop_top = 0, crop_bottom = 0;
303 for (
int x = 0; x <
width; ++x) {
305 for (
int y = 0; y <
height; ++y) {
306 if (!is_bg(x, y)) { all_bg =
false;
break; }
314 for (
int x =
width - 1; x >= crop_left; --x) {
316 for (
int y = 0; y <
height; ++y) {
317 if (!is_bg(x, y)) { all_bg =
false;
break; }
320 crop_right =
width - x;
325 for (
int y = 0; y <
height; ++y) {
327 for (
int x = 0; x <
width; ++x) {
328 if (!is_bg(x, y)) { all_bg =
false;
break; }
336 for (
int y =
height - 1; y >= crop_top; --y) {
338 for (
int x = 0; x <
width; ++x) {
339 if (!is_bg(x, y)) { all_bg =
false;
break; }
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));
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);
364 auto get = [
this](
int px,
int py) ->
Color {
365 int idx = (py *
width + px) * 4;
369 Color c00 = get(x0, y0);
Color c10 = get(x1, y0);
370 Color c01 = get(x0, y1);
Color c11 = get(x1, y1);
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);
380 if (contrast == 1.0f)
return;
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);
393 std::ofstream f(filename, std::ios::binary);
396 f <<
"P6\n" <<
width <<
" " <<
height <<
"\n255\n";
407 int row_size =
width * 4;
408 int pixel_data_size = row_size *
height;
409 int file_size = 14 + 40 + 56 + pixel_data_size;
411 std::ofstream f(filename, std::ios::binary);
416 uint8_t fh[14] = {0};
419 std::memcpy(fh + 2, &file_size, 4);
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);
427 uint32_t header_size = 56;
428 f.write(
reinterpret_cast<const char *
>(&header_size), 4);
431 f.write(
reinterpret_cast<const char *
>(&w), 4);
432 f.write(
reinterpret_cast<const char *
>(&h), 4);
434 f.write(
reinterpret_cast<const char *
>(&planes), 2);
436 f.write(
reinterpret_cast<const char *
>(&bpp), 2);
437 uint32_t compression = 3;
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);
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);
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);
459 for (
int y =
height - 1; y >= 0; --y) {
460 for (
int x = 0; x <
width; ++x) {
461 int idx = (y *
width + x) * 4;
475 const int bytes_per_pixel = use24bit ? 3 : 4;
477 std::ofstream f(filename, std::ios::binary);
487 uint8_t header[18] = {0};
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);
495 if (!use24bit) header[17] |= 0x08;
497 f.write(
reinterpret_cast<const char *
>(header),
sizeof(header));
503 std::vector<uint8_t> row(
static_cast<size_t>(
width) * bytes_per_pixel);
504 for (
int y = 0; y <
height; ++y) {
506 for (
int x = 0; x <
width; ++x) {
507 int idx = (y *
width + x) * 4;
508 row[o++] =
pixels[idx + 2];
509 row[o++] =
pixels[idx + 1];
511 if (!use24bit) row[o++] =
pixels[idx + 3];
513 f.write(
reinterpret_cast<const char *
>(row.data()),
514 static_cast<std::streamsize
>(row.size()));
524 int stride =
width * 4;
525 return stbi_write_png(filename.c_str(),
width,
height, 4,
526 pixels.data(), stride) != 0;
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();
538 std::memcpy(img.
pixels.data(), data, w * h * 4);
539 stbi_image_free(data);
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);
563 const Color &background) {
564 if (images.empty())
return Image();
566 int n =
static_cast<int>(images.size());
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;
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);
584 if (cell_w <= 0 || cell_h <= 0)
return Image();
586 bool is_1d_horizontal = (nrow == 1);
587 bool is_1d_vertical = (ncol == 1);
592 std::vector<Image> cells;
595 if (is_1d_horizontal) {
597 for (
const auto &img : images) {
600 float ar =
static_cast<float>(img.width) / img.
height;
601 c.
scale(
static_cast<int>(cell_h * ar), cell_h);
605 cells.push_back(std::move(c));
607 ncol =
static_cast<int>(cells.size());
609 }
else if (is_1d_vertical) {
611 for (
const auto &img : images) {
614 float ar =
static_cast<float>(img.height) / img.
width;
615 c.
scale(cell_w,
static_cast<int>(cell_w * ar));
619 cells.push_back(std::move(c));
621 nrow =
static_cast<int>(cells.size());
625 for (
const auto &img : images) {
628 c.
scale(cell_w, cell_h);
632 cells.push_back(std::move(c));
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);
644 std::vector<Image> rows;
646 for (
int r = 0; r < nrow; ++r) {
647 Image row_img = cells[r * ncol];
648 for (
int c = 1; c < ncol; ++c) {
651 rows.push_back(std::move(row_img));
657 Image result = rows[0];
658 for (
int r = 1; r < nrow; ++r) {
The Image — an RGBA pixel buffer with compositing and I/O operations.
CropContentDirection
Direction(s) for the crop_to_content() operation.
@ BOTTOM
Crop bottom edge only.
@ RIGHT
Crop right edge only.
@ VERTICAL
Crop both top and bottom edges.
@ ALL
Crop all four edges.
@ 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.
MergeDirection
Direction for the merge() operation.
@ BOTTOM
Attach other below.
@ RIGHT
Attach other to the right side.
@ LEFT
Attach other to the left side.
FitMode
Strategy for normalizing images to a common cell size in grid_arrange().
@ SCALE
Scale all images to match the largest cell dimensions.
An RGBA color with floating-point components.
float g
Green channel, [0, 1].
float r
Red channel, [0, 1].
float b
Blue channel, [0, 1].
float a
Alpha (opacity) channel, [0, 1]. 1.0 = fully opaque.
void apply_contrast(float contrast)
Apply a contrast adjustment to the image.
int height
Image height in pixels.
bool write_tga(const std::string &filename, bool use24bit=false) const
Write the image as a TGA file (Truevision Targa).
bool write_ppm(const std::string &filename) const
Write the image as a PPM file (Portable Pixmap).
void scale(int new_width, int new_height)
Scale (resize) the image to new dimensions in-place.
int width
Image width in pixels.
void crop(int x, int y, int w, int h)
Crop the image to a sub-rectangle.
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.
std::vector< uint8_t > pixels
Raw pixel data: RGBA bytes, row-major, bottom-left origin.
void grow(int top, int bottom, int left, int right, const Color &background)
Grow (pad) the image by adding borders.
void rotate_90(bool clockwise=true)
Rotate the image by 90 degrees in-place.
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).
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.
void merge(const Image &other, MergeDirection direction)
Merge (concatenate) another image onto this one.
Image downsample_box(int factor) const
Downsample the image by a factor using box filtering.
void pad_to_size(int target_w, int target_h, const Color &background)
Pad the image to a target size, centering the content.
bool write_png(const std::string &filename) const
Write the image as a PNG file.
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).
void crop_to_content(CropContentDirection direction, const Color &background)
Crop away uniform borders of a given background color.
Color sample_bilinear(float u, float v) const
Sample the image at texture coordinates (u, v) using bilinear interpolation.
static Image read_image(const std::string &path)
Read an image from a file (PNG, BMP, TGA, JPEG, etc.).
bool write_bmp(const std::string &filename) const
Write the image as a BMP file (Windows Bitmap).