/ tests / src / imgs / scale.cpp
scale.cpp
  1  /* ************************************************************************** */
  2  /*                                                                            */
  3  /*                                                        :::      ::::::::   */
  4  /*   scale.cpp                                          :+:      :+:    :+:   */
  5  /*                                                    +:+ +:+         +:+     */
  6  /*   By: lfiorell <lfiorell@student.42.fr>          +#+  +:+       +#+        */
  7  /*                                                +#+#+#+#+#+   +#+           */
  8  /*   Created: 2025/02/13 21:13:24 by lfiorell          #+#    #+#             */
  9  /*   Updated: 2025/02/14 12:23:47 by lfiorell         ###   ########.fr       */
 10  /*                                                                            */
 11  /* ************************************************************************** */
 12  
 13  #include "CUnit/Basic.h"
 14  #include <unistd.h> // Added for usleep
 15  
 16  extern "C"
 17  {
 18  #include "img/scale.h"
 19  #include "mlx.h"
 20  #include <time.h>
 21  }
 22  
 23  #define CU_ASSERT_RGB_EQUAL(actual, expected) \
 24    {                                           \
 25      CU_ASSERT_EQUAL(actual.r, expected.r);    \
 26      CU_ASSERT_EQUAL(actual.g, expected.g);    \
 27      CU_ASSERT_EQUAL(actual.b, expected.b);    \
 28    }
 29  
 30  #define TOLERANCE 100                         // Allowable difference in each color channel
 31  #define SHOW_DIFFS 0                          // Set to 1 to print out differences and show windows
 32  #define MAX_EXECUTION_TIME_SEC (1.0f / 30.0f) // Maximum allowed execution time in seconds
 33  
 34  #if SHOW_DIFFS
 35  // Helper function to display images in two windows and wait for user input in the console
 36  static void display_images(void *mlx, t_img *src, t_img *dst)
 37  {
 38    // Determine which image is bigger
 39    t_img *first = (src->width * src->height > dst->width * dst->height) ? src : dst;
 40    t_img *second = (first == src) ? dst : src;
 41  
 42    const char *name_first = (first == src) ? "src" : "dst";
 43    const char *name_second = (second == src) ? "src" : "dst";
 44  
 45    void *win_first = mlx_new_window(mlx, first->width, first->height, (char *)name_first);
 46    void *win_second = mlx_new_window(mlx, second->width, second->height, (char *)name_second);
 47  
 48    mlx_put_image_to_window(mlx, win_first, first->img_ptr, 0, 0);
 49    mlx_put_image_to_window(mlx, win_second, second->img_ptr, 0, 0);
 50  
 51    printf("\n        Enter Y for CU_PASS or N for CU_FAIL: ");
 52    int ch = getchar();
 53    while (getchar() != '\n')
 54    {
 55    } // Clear the input buffer
 56  
 57    if (ch == 'Y' || ch == 'y')
 58      printf("CU_PASS\n");
 59    else if (ch == 'N' || ch == 'n')
 60      printf("CU_FAIL\n");
 61    else
 62      printf("Invalid input. Defaulting to CU_FAIL\n");
 63  
 64    mlx_destroy_window(mlx, win_first);
 65    mlx_destroy_window(mlx, win_second);
 66  }
 67  #endif
 68  
 69  // Helper function: compare two channel values with tolerance
 70  static int channels_equal(unsigned char a, unsigned char b)
 71  {
 72    return (abs((int)a - (int)b) <= TOLERANCE);
 73  }
 74  
 75  static void assert_rgba_equal(t_rgba expected, t_rgba actual)
 76  {
 77    if (!channels_equal(expected.r, actual.r) ||
 78        !channels_equal(expected.g, actual.g) ||
 79        !channels_equal(expected.b, actual.b) ||
 80        !channels_equal(expected.a, actual.a))
 81    {
 82      printf("\nExpected: R=%d G=%d B=%d A=%d\n", expected.r, expected.g, expected.b, expected.a);
 83      printf("Actual  : R=%d G=%d B=%d A=%d\n", actual.r, actual.g, actual.b, actual.a);
 84      printf("Diffs   : R=%d G=%d B=%d A=%d\n",
 85             abs((int)expected.r - (int)actual.r),
 86             abs((int)expected.g - (int)actual.g),
 87             abs((int)expected.b - (int)actual.b),
 88             abs((int)expected.a - (int)actual.a));
 89    }
 90    CU_ASSERT_TRUE_FATAL(channels_equal(expected.r, actual.r));
 91    CU_ASSERT_TRUE_FATAL(channels_equal(expected.g, actual.g));
 92    CU_ASSERT_TRUE_FATAL(channels_equal(expected.b, actual.b));
 93    CU_ASSERT_TRUE_FATAL(channels_equal(expected.a, actual.a));
 94  }
 95  
 96  static void assert_execution_time(clock_t start, clock_t end)
 97  {
 98    double cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC;
 99    if (cpu_time_used > MAX_EXECUTION_TIME_SEC)
100    {
101      printf("\nWARNING: Execution time: %.6f seconds (exceeded %.6f seconds limit of %.6f seconds)\n",
102             cpu_time_used, MAX_EXECUTION_TIME_SEC, cpu_time_used - MAX_EXECUTION_TIME_SEC);
103      CU_ASSERT_TRUE(1); // Still pass the test, but issue a warning
104    }
105    else
106    {
107      CU_ASSERT_TRUE(cpu_time_used <= MAX_EXECUTION_TIME_SEC);
108    }
109  }
110  
111  #pragma region Nearest Neighbor
112  void test_img_scale_nearest_up_2x(void)
113  {
114    char *path = (char *)"data/image.xpm";
115    void *mlx = mlx_init();
116  
117    t_img *img = crust_img_from_xpm(mlx, path);
118    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
119  
120    t_2d new_size = {img->width * 2, img->height * 2};
121  
122    // Start timing before scale operation
123    clock_t start = clock();
124    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST);
125    clock_t end = clock();
126  
127    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
128    assert_execution_time(start, end);
129  
130    CU_ASSERT_EQUAL(dst->width, img->width * 2);
131    CU_ASSERT_EQUAL(dst->height, img->height * 2);
132  
133    for (int y = 0; y < img->height; ++y)
134    {
135      for (int x = 0; x < img->width; ++x)
136      {
137        t_2d pos = {x * 2, y * 2};
138        t_2d src_pos = {x, y};
139        t_rgba src_pixel = crust_img_get_pixel(img, src_pos);
140        t_rgba dst_pixel = crust_img_get_pixel(dst, pos);
141  
142        CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel);
143      }
144    }
145  
146  #if SHOW_DIFFS
147    display_images(mlx, img, dst);
148  #endif
149  
150    crust_img_drop(img);
151    crust_img_drop(dst);
152  }
153  
154  void test_img_scale_nearest_down_2x(void)
155  {
156    char *path = (char *)"data/image.xpm";
157    void *mlx = mlx_init();
158  
159    t_img *img = crust_img_from_xpm(mlx, path);
160    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
161  
162    t_2d new_size = {img->width / 2, img->height / 2};
163  
164    // Start timing before scale operation
165    clock_t start = clock();
166    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST);
167    clock_t end = clock();
168  
169    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
170    assert_execution_time(start, end);
171  
172    CU_ASSERT_EQUAL(dst->width, img->width / 2);
173    CU_ASSERT_EQUAL(dst->height, img->height / 2);
174  
175    for (int y = 0; y < dst->height; ++y)
176    {
177      for (int x = 0; x < dst->width; ++x)
178      {
179        t_2d pos = {x, y};
180        t_2d src_pos = {x * 2, y * 2};
181        t_rgba src_pixel = crust_img_get_pixel(img, src_pos);
182        t_rgba dst_pixel = crust_img_get_pixel(dst, pos);
183  
184        CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel);
185      }
186    }
187  
188  #if SHOW_DIFFS
189    display_images(mlx, img, dst);
190  #endif
191  
192    crust_img_drop(img);
193    crust_img_drop(dst);
194  }
195  
196  void test_img_scale_nearest_up_1_5x(void)
197  {
198    char *path = (char *)"data/image.xpm";
199    void *mlx = mlx_init();
200  
201    double gscale = 1.5;
202    float fgscale = gscale;
203  
204    t_img *img = crust_img_from_xpm(mlx, path);
205    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
206  
207    t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)};
208  
209    // Start timing before scale operation
210    clock_t start = clock();
211    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST);
212    clock_t end = clock();
213  
214    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
215    assert_execution_time(start, end);
216  
217    CU_ASSERT_EQUAL(dst->width, new_size.x);
218    CU_ASSERT_EQUAL(dst->height, new_size.y);
219  
220    const float scale = fgscale;
221    const float inv_scale = 1.0f / scale;
222  
223    /* Instead of iterating over the source image and mapping forward,
224       iterate over every pixel in the destination and compute the corresponding
225       source pixel (using nearest-neighbor, which is simply a cast after scaling). */
226    for (int dst_y = 0; dst_y < dst->height; ++dst_y)
227    {
228      int src_y = (int)(dst_y * inv_scale);
229      if (src_y >= img->height)
230        src_y = img->height - 1;
231  
232      for (int dst_x = 0; dst_x < dst->width; ++dst_x)
233      {
234        int src_x = (int)(dst_x * inv_scale);
235        if (src_x >= img->width)
236          src_x = img->width - 1;
237  
238        t_2d src_pos = {src_x, src_y};
239        t_2d dst_pos = {dst_x, dst_y};
240  
241        t_rgba src_pixel = crust_img_get_pixel(img, src_pos);
242        t_rgba dst_pixel = crust_img_get_pixel(dst, dst_pos);
243  
244        CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel);
245      }
246    }
247  
248  #if SHOW_DIFFS
249    display_images(mlx, img, dst);
250  #endif
251  
252    crust_img_drop(img);
253    crust_img_drop(dst);
254  }
255  
256  void test_img_scale_nearest_down_0_5x(void)
257  {
258    char *path = (char *)"data/image.xpm";
259    void *mlx = mlx_init();
260  
261    double gscale = 0.5;
262    float fgscale = gscale;
263  
264    t_img *img = crust_img_from_xpm(mlx, path);
265    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
266  
267    // Calculate new (scaled down) size
268    t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)};
269  
270    // Start timing before scale operation
271    clock_t start = clock();
272    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_NEAREST);
273    clock_t end = clock();
274  
275    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
276    assert_execution_time(start, end);
277  
278    CU_ASSERT_EQUAL(dst->width, new_size.x);
279    CU_ASSERT_EQUAL(dst->height, new_size.y);
280  
281    const float scale = fgscale;
282    const float inv_scale = 1.0f / scale; // inv_scale = 2.0
283  
284    // Iterate over every pixel in the destination image.
285    // For each destination pixel, compute its corresponding source pixel.
286    for (int dst_y = 0; dst_y < dst->height; ++dst_y)
287    {
288      // Calculate the corresponding source y position via the inverse scale factor.
289      int src_y = (int)(dst_y * inv_scale);
290      if (src_y >= img->height)
291        src_y = img->height - 1;
292  
293      for (int dst_x = 0; dst_x < dst->width; ++dst_x)
294      {
295        // Calculate the corresponding source x position.
296        int src_x = (int)(dst_x * inv_scale);
297        if (src_x >= img->width)
298          src_x = img->width - 1;
299  
300        t_2d src_pos = {src_x, src_y};
301        t_2d dst_pos = {dst_x, dst_y};
302  
303        t_rgba src_pixel = crust_img_get_pixel(img, src_pos);
304        t_rgba dst_pixel = crust_img_get_pixel(dst, dst_pos);
305  
306        CU_ASSERT_RGB_EQUAL(src_pixel, dst_pixel);
307      }
308    }
309  
310  #if SHOW_DIFFS
311    display_images(mlx, img, dst);
312  #endif
313  
314    crust_img_drop(img);
315    crust_img_drop(dst);
316  }
317  #pragma endregion
318  
319  #pragma region Bilenaer
320  
321  // Helper function to clamp coordinates within the image boundaries
322  static inline int clamp(int x, int lower, int upper)
323  {
324    if (x < lower)
325      return lower;
326    else if (x > upper)
327      return upper;
328    else
329      return x;
330  }
331  
332  // Helper function: bilinear interpolation of four channel values given fractional weights
333  static unsigned char bilinear_interp_channel(unsigned char c00, unsigned char c10,
334                                               unsigned char c01, unsigned char c11,
335                                               float frac_x, float frac_y)
336  {
337    float top = c00 * (1.0f - frac_x) + c10 * frac_x;
338    float bottom = c01 * (1.0f - frac_x) + c11 * frac_x;
339    float value = top * (1.0f - frac_y) + bottom * frac_y;
340    return (unsigned char)(value + 0.5f);
341  }
342  
343  // Helper function: compute expected bilinear-interpolated pixel from the source image
344  static t_rgba compute_expected_bilinear(t_img *img, float src_x, float src_y)
345  {
346    // The four neighboring pixels:
347    int x0 = clamp((int)src_x, 0, img->width - 1);
348    int x1 = clamp(x0 + 1, 0, img->width - 1);
349    int y0 = clamp((int)src_y, 0, img->height - 1);
350    int y1 = clamp(y0 + 1, 0, img->height - 1);
351  
352    // Fractional parts within the pixel
353    float frac_x = src_x - (float)x0;
354    float frac_y = src_y - (float)y0;
355  
356    t_2d p00 = {x0, y0};
357    t_2d p10 = {x1, y0};
358    t_2d p01 = {x0, y1};
359    t_2d p11 = {x1, y1};
360  
361    t_rgba c00 = crust_img_get_pixel(img, p00);
362    t_rgba c10 = crust_img_get_pixel(img, p10);
363    t_rgba c01 = crust_img_get_pixel(img, p01);
364    t_rgba c11 = crust_img_get_pixel(img, p11);
365  
366    t_rgba result;
367    result.r = bilinear_interp_channel(c00.r, c10.r, c01.r, c11.r, frac_x, frac_y);
368    result.g = bilinear_interp_channel(c00.g, c10.g, c01.g, c11.g, frac_x, frac_y);
369    result.b = bilinear_interp_channel(c00.b, c10.b, c01.b, c11.b, frac_x, frac_y);
370    result.a = bilinear_interp_channel(c00.a, c10.a, c01.a, c11.a, frac_x, frac_y);
371  
372    return result;
373  }
374  
375  // Test function for bilinear upscaling by 2x
376  void test_img_scale_bilinear_up_2x(void)
377  {
378    char *path = (char *)"data/image.xpm";
379    void *mlx = mlx_init();
380    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
381  
382    t_img *img = crust_img_from_xpm(mlx, path);
383    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
384  
385    // Target size: double both dimensions
386    t_2d new_size = {img->width * 2, img->height * 2};
387  
388    // Start timing before scale operation
389    clock_t start = clock();
390    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR);
391    clock_t end = clock();
392  
393    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
394    assert_execution_time(start, end);
395  
396    CU_ASSERT_EQUAL(dst->width, new_size.x);
397    CU_ASSERT_EQUAL(dst->height, new_size.y);
398  
399    // For each pixel in the destination image,
400    // compute the corresponding source coordinate.
401    // Note: When scaling up 2x, the mapping is:
402    // src_x = dst_x / 2.0 and src_y = dst_y / 2.0
403    for (int dst_y = 0; dst_y < dst->height; ++dst_y)
404    {
405      for (int dst_x = 0; dst_x < dst->width; ++dst_x)
406      {
407        float src_x = dst_x / 2.0f;
408        float src_y = dst_y / 2.0f;
409  
410        // Compute what the pixel should be from a bilinear interpolation of the source
411        t_2d dst_pos = {dst_x, dst_y};
412        t_rgba expected = compute_expected_bilinear(img, src_x, src_y);
413        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
414  
415        // Compare each channel with tolerance (if needed)
416        assert_rgba_equal(expected, actual);
417      }
418    }
419  
420  #if SHOW_DIFFS
421    display_images(mlx, img, dst);
422  #endif
423  
424    crust_img_drop(img);
425    crust_img_drop(dst);
426  }
427  
428  // Test function for bilinear downscaling by 2x
429  void test_img_scale_bilinear_down_2x(void)
430  {
431    char *path = (char *)"data/image.xpm";
432    void *mlx = mlx_init();
433    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
434  
435    t_img *img = crust_img_from_xpm(mlx, path);
436    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
437  
438    // Target size: half both dimensions
439    t_2d new_size = {img->width / 2, img->height / 2};
440  
441    // Start timing before scale operation
442    clock_t start = clock();
443    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR);
444    clock_t end = clock();
445  
446    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
447    assert_execution_time(start, end);
448  
449    CU_ASSERT_EQUAL(dst->width, new_size.x);
450    CU_ASSERT_EQUAL(dst->height, new_size.y);
451  
452    // For each pixel in the destination image,
453    // compute the corresponding source coordinate.
454    // Note: When scaling down 2x, the mapping is:
455    // src_x = dst_x * 2.0 and src_y = dst_y * 2.0
456    for (int dst_y = 0; dst_y < dst->height; ++dst_y)
457    {
458      for (int dst_x = 0; dst_x < dst->width; ++dst_x)
459      {
460        float src_x = dst_x * 2.0f;
461        float src_y = dst_y * 2.0f;
462  
463        // Compute what the pixel should be from a bilinear interpolation of the source
464        t_2d dst_pos = {dst_x, dst_y};
465        t_rgba expected = compute_expected_bilinear(img, src_x, src_y);
466        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
467  
468        // Compare each channel with tolerance (if needed)
469        assert_rgba_equal(expected, actual);
470      }
471    }
472  
473  #if SHOW_DIFFS
474    display_images(mlx, img, dst);
475  #endif
476  
477    crust_img_drop(img);
478    crust_img_drop(dst);
479  }
480  
481  // Test function for bilinear upscaling by 1.5x
482  void test_img_scale_bilinear_up_1_5x(void)
483  {
484    char *path = (char *)"data/image.xpm";
485    void *mlx = mlx_init();
486    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
487  
488    double gscale = 1.5;
489    float fgscale = gscale;
490  
491    t_img *img = crust_img_from_xpm(mlx, path);
492    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
493  
494    // Target size: 1.5x both dimensions
495    t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)};
496  
497    // Start timing before scale operation
498    clock_t start = clock();
499    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR);
500    clock_t end = clock();
501  
502    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
503    assert_execution_time(start, end);
504  
505    CU_ASSERT_EQUAL(dst->width, new_size.x);
506    CU_ASSERT_EQUAL(dst->height, new_size.y);
507  
508    const float scale = fgscale;
509    const float inv_scale = 1.0f / scale;
510  
511    // For each pixel in the destination image,
512    // compute the corresponding source coordinate.
513    for (int dst_y = 0; dst_y < dst->height; ++dst_y)
514    {
515      for (int dst_x = 0; dst_x < dst->width; ++dst_x)
516      {
517        float src_x = dst_x * inv_scale;
518        float src_y = dst_y * inv_scale;
519  
520        // Compute what the pixel should be from a bilinear interpolation of the source
521        t_2d dst_pos = {dst_x, dst_y};
522        t_rgba expected = compute_expected_bilinear(img, src_x, src_y);
523        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
524  
525        // Compare each channel with tolerance (if needed)
526        assert_rgba_equal(expected, actual);
527      }
528    }
529  
530  #if SHOW_DIFFS
531    display_images(mlx, img, dst);
532  #endif
533  
534    crust_img_drop(img);
535    crust_img_drop(dst);
536  }
537  
538  // Test function for bilinear downscaling by 0.5x
539  void test_img_scale_bilinear_down_0_5x(void)
540  {
541    char *path = (char *)"data/image.xpm";
542    void *mlx = mlx_init();
543    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
544  
545    double gscale = 0.5;
546    float fgscale = gscale;
547  
548    t_img *img = crust_img_from_xpm(mlx, path);
549    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
550  
551    // Target size: half both dimensions
552    t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)};
553  
554    // Start timing before scale operation
555    clock_t start = clock();
556    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_BILINEAR);
557    clock_t end = clock();
558  
559    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
560    assert_execution_time(start, end);
561  
562    CU_ASSERT_EQUAL(dst->width, new_size.x);
563    CU_ASSERT_EQUAL(dst->height, new_size.y);
564  
565    const float scale = fgscale;
566    const float inv_scale = 1.0f / scale;
567  
568    // For each pixel in the destination image,
569    // compute the corresponding source coordinate.
570    for (int dst_y = 0; dst_y < dst->height; ++dst_y)
571    {
572      for (int dst_x = 0; dst_x < dst->width; ++dst_x)
573      {
574        float src_x = dst_x * inv_scale;
575        float src_y = dst_y * inv_scale;
576  
577        // Compute what the pixel should be from a bilinear interpolation of the source
578        t_2d dst_pos = {dst_x, dst_y};
579        t_rgba expected = compute_expected_bilinear(img, src_x, src_y);
580        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
581  
582        // Compare each channel with tolerance (if needed)
583        assert_rgba_equal(expected, actual);
584      }
585    }
586  
587  #if SHOW_DIFFS
588    display_images(mlx, img, dst);
589  #endif
590  
591    crust_img_drop(img);
592    crust_img_drop(dst);
593  }
594  #pragma endregion
595  
596  #pragma region Lanczos
597  // Lanczos kernel with a=2 (using 4 samples total)
598  static float lanczos2(float x)
599  {
600    if (x == 0.0f)
601      return 1.0f;
602    if (x < -2.0f || x > 2.0f)
603      return 0.0f;
604  
605    x *= M_PI;
606    return (sin(x) * sin(x / 2.0f)) / (x * x / 2.0f);
607  }
608  
609  // Helper function: compute expected Lanczos-interpolated pixel from the source image
610  static t_rgba compute_expected_lanczos(t_img *img, float src_x, float src_y)
611  {
612    float r = 0.0f, g = 0.0f, b = 0.0f, a = 0.0f;
613    float weight_sum = 0.0f;
614  
615    // Sample a 4x4 neighborhood centered around the source coordinate
616    int start_x = (int)src_x - 2;
617    int start_y = (int)src_y - 2;
618  
619    for (int y = 0; y < 4; y++)
620    {
621      int sample_y = clamp(start_y + y, 0, img->height - 1);
622      float dy = src_y - (start_y + y);
623  
624      for (int x = 0; x < 4; x++)
625      {
626        int sample_x = clamp(start_x + x, 0, img->width - 1);
627        float dx = src_x - (start_x + x);
628  
629        // Calculate the 2D Lanczos weight
630        float weight = lanczos2(dx) * lanczos2(dy);
631        t_2d pos = {sample_x, sample_y};
632        t_rgba pixel = crust_img_get_pixel(img, pos);
633  
634        r += pixel.r * weight;
635        g += pixel.g * weight;
636        b += pixel.b * weight;
637        a += pixel.a * weight;
638        weight_sum += weight;
639      }
640    }
641  
642    // Normalize by weight sum to account for edge cases
643    float inv_weight = 1.0f / weight_sum;
644    t_rgba result;
645    result.r = (unsigned char)(clamp((int)(r * inv_weight + 0.5f), 0, 255));
646    result.g = (unsigned char)(clamp((int)(g * inv_weight + 0.5f), 0, 255));
647    result.b = (unsigned char)(clamp((int)(b * inv_weight + 0.5f), 0, 255));
648    result.a = (unsigned char)(clamp((int)(a * inv_weight + 0.5f), 0, 255));
649  
650    return result;
651  }
652  
653  // Test function for Lanczos upscaling by 2x
654  void test_img_scale_lanczos_up_2x(void)
655  {
656    char *path = (char *)"data/image.xpm";
657    void *mlx = mlx_init();
658    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
659  
660    t_img *img = crust_img_from_xpm(mlx, path);
661    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
662  
663    // Ensure source image has valid dimensions
664    CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0);
665  
666    // Target size: double both dimensions
667    t_2d new_size = {img->width * 2, img->height * 2};
668  
669    // Start timing before scale operation
670    clock_t start = clock();
671    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS);
672    clock_t end = clock();
673  
674    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
675    assert_execution_time(start, end);
676  
677    CU_ASSERT_EQUAL(dst->width, new_size.x);
678    CU_ASSERT_EQUAL(dst->height, new_size.y);
679  
680    const float scale_factor = 2.0f;
681    const float inv_scale = 1.0f / scale_factor;
682  
683    // Only test a subset of pixels to avoid long test times
684    const int step = 4; // Test every 4th pixel
685    for (int dst_y = 0; dst_y < dst->height; dst_y += step)
686    {
687      for (int dst_x = 0; dst_x < dst->width; dst_x += step)
688      {
689        float src_x = dst_x * inv_scale;
690        float src_y = dst_y * inv_scale;
691  
692        // Ensure source coordinates are within bounds
693        if (src_x >= img->width - 2 || src_y >= img->height - 2)
694          continue;
695  
696        t_2d dst_pos = {dst_x, dst_y};
697        t_rgba expected = compute_expected_lanczos(img, src_x, src_y);
698        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
699  
700        // Compare each channel with tolerance
701        assert_rgba_equal(expected, actual);
702      }
703    }
704  
705  #if SHOW_DIFFS
706    display_images(mlx, img, dst);
707  #endif
708  
709    crust_img_drop(img);
710    crust_img_drop(dst);
711  }
712  
713  // Test function for Lanczos downscaling by 2x
714  void test_img_scale_lanczos_down_2x(void)
715  {
716    char *path = (char *)"data/image.xpm";
717    void *mlx = mlx_init();
718    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
719  
720    t_img *img = crust_img_from_xpm(mlx, path);
721    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
722  
723    // Ensure source image has valid dimensions
724    CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0);
725  
726    // Target size: half both dimensions
727    t_2d new_size = {img->width / 2, img->height / 2};
728  
729    // Start timing before scale operation
730    clock_t start = clock();
731    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS);
732    clock_t end = clock();
733  
734    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
735    assert_execution_time(start, end);
736  
737    CU_ASSERT_EQUAL(dst->width, new_size.x);
738    CU_ASSERT_EQUAL(dst->height, new_size.y);
739  
740    const float scale_factor = 0.5f;
741    const float inv_scale = 1.0f / scale_factor;
742  
743    // Only test a subset of pixels to avoid long test times
744    const int step = 4; // Test every 4th pixel
745    for (int dst_y = 0; dst_y < dst->height; dst_y += step)
746    {
747      for (int dst_x = 0; dst_x < dst->width; dst_x += step)
748      {
749        float src_x = dst_x * inv_scale;
750        float src_y = dst_y * inv_scale;
751  
752        // Ensure source coordinates are within bounds
753        if (src_x >= img->width - 2 || src_y >= img->height - 2)
754          continue;
755  
756        t_2d dst_pos = {dst_x, dst_y};
757        t_rgba expected = compute_expected_lanczos(img, src_x, src_y);
758        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
759  
760        // Compare each channel with tolerance
761        assert_rgba_equal(expected, actual);
762      }
763    }
764  
765  #if SHOW_DIFFS
766    display_images(mlx, img, dst);
767  #endif
768  
769    crust_img_drop(img);
770    crust_img_drop(dst);
771  }
772  
773  // Test function for Lanczos upscaling by 1.5x
774  void test_img_scale_lanczos_up_1_5x(void)
775  {
776    char *path = (char *)"data/image.xpm";
777    void *mlx = mlx_init();
778    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
779  
780    double gscale = 1.5;
781    float fgscale = gscale;
782  
783    t_img *img = crust_img_from_xpm(mlx, path);
784    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
785  
786    // Ensure source image has valid dimensions
787    CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0);
788  
789    // Target size: 1.5x both dimensions
790    t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)};
791  
792    // Start timing before scale operation
793    clock_t start = clock();
794    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS);
795    clock_t end = clock();
796  
797    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
798    assert_execution_time(start, end);
799  
800    CU_ASSERT_EQUAL(dst->width, new_size.x);
801    CU_ASSERT_EQUAL(dst->height, new_size.y);
802  
803    const float scale = fgscale;
804    const float inv_scale = 1.0f / scale;
805  
806    // Only test a subset of pixels to avoid long test times
807    const int step = 4; // Test every 4th pixel
808    for (int dst_y = 0; dst_y < dst->height; dst_y += step)
809    {
810      for (int dst_x = 0; dst_x < dst->width; dst_x += step)
811      {
812        float src_x = dst_x * inv_scale;
813        float src_y = dst_y * inv_scale;
814  
815        // Ensure source coordinates are within bounds
816        if (src_x >= img->width - 2 || src_y >= img->height - 2)
817          continue;
818  
819        t_2d dst_pos = {dst_x, dst_y};
820        t_rgba expected = compute_expected_lanczos(img, src_x, src_y);
821        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
822  
823        // Compare each channel with tolerance
824        assert_rgba_equal(expected, actual);
825      }
826    }
827  
828  #if SHOW_DIFFS
829    display_images(mlx, img, dst);
830  #endif
831  
832    crust_img_drop(img);
833    crust_img_drop(dst);
834  }
835  
836  // Test function for Lanczos downscaling by 0.5x
837  void test_img_scale_lanczos_down_0_5x(void)
838  {
839    char *path = (char *)"data/image.xpm";
840    void *mlx = mlx_init();
841    CU_ASSERT_PTR_NOT_NULL_FATAL(mlx);
842  
843    double gscale = 0.5;
844    float fgscale = gscale;
845  
846    t_img *img = crust_img_from_xpm(mlx, path);
847    CU_ASSERT_PTR_NOT_NULL_FATAL(img);
848  
849    // Ensure source image has valid dimensions
850    CU_ASSERT_TRUE_FATAL(img->width > 0 && img->height > 0);
851  
852    // Target size: half both dimensions
853    t_2d new_size = {(int)(img->width * gscale), (int)(img->height * gscale)};
854  
855    // Start timing before scale operation
856    clock_t start = clock();
857    t_img *dst = crust_img_scale(img, new_size, CRUST_IMG_SCALE_LANCZOS);
858    clock_t end = clock();
859  
860    CU_ASSERT_PTR_NOT_NULL_FATAL(dst);
861    assert_execution_time(start, end);
862  
863    CU_ASSERT_EQUAL(dst->width, new_size.x);
864    CU_ASSERT_EQUAL(dst->height, new_size.y);
865  
866    const float scale = fgscale;
867    const float inv_scale = 1.0f / scale;
868  
869    // Only test a subset of pixels to avoid long test times
870    const int step = 4; // Test every 4th pixel
871    for (int dst_y = 0; dst_y < dst->height; dst_y += step)
872    {
873      for (int dst_x = 0; dst_x < dst->width; dst_x += step)
874      {
875        float src_x = dst_x * inv_scale;
876        float src_y = dst_y * inv_scale;
877  
878        // Ensure source coordinates are within bounds
879        if (src_x >= img->width - 2 || src_y >= img->height - 2)
880          continue;
881  
882        t_2d dst_pos = {dst_x, dst_y};
883        t_rgba expected = compute_expected_lanczos(img, src_x, src_y);
884        t_rgba actual = crust_img_get_pixel(dst, dst_pos);
885  
886        // Compare each channel with tolerance
887        assert_rgba_equal(expected, actual);
888      }
889    }
890  
891  #if SHOW_DIFFS
892    display_images(mlx, img, dst);
893  #endif
894  
895    crust_img_drop(img);
896    crust_img_drop(dst);
897  }
898  #pragma endregion
899  
900  void run_scale_tests(void)
901  {
902    CU_pSuite suite = CU_add_suite("Scale", NULL, NULL);
903    CU_add_test(suite, "test_img_scale_nearest_up_2x", test_img_scale_nearest_up_2x);
904    CU_add_test(suite, "test_img_scale_nearest_down_2x", test_img_scale_nearest_down_2x);
905    CU_add_test(suite, "test_img_scale_nearest_up_1_5x", test_img_scale_nearest_up_1_5x);
906    CU_add_test(suite, "test_img_scale_nearest_down_0_5x", test_img_scale_nearest_down_0_5x);
907  
908    CU_add_test(suite, "test_img_scale_bilinear_up_2x", test_img_scale_bilinear_up_2x);
909    CU_add_test(suite, "test_img_scale_bilinear_down_2x", test_img_scale_bilinear_down_2x);
910    CU_add_test(suite, "test_img_scale_bilinear_up_1_5x", test_img_scale_bilinear_up_1_5x);
911    CU_add_test(suite, "test_img_scale_bilinear_down_0_5x", test_img_scale_bilinear_down_0_5x);
912  
913    CU_add_test(suite, "test_img_scale_lanczos_up_2x", test_img_scale_lanczos_up_2x);
914    CU_add_test(suite, "test_img_scale_lanczos_down_2x", test_img_scale_lanczos_down_2x);
915    CU_add_test(suite, "test_img_scale_lanczos_up_1_5x", test_img_scale_lanczos_up_1_5x);
916    CU_add_test(suite, "test_img_scale_lanczos_down_0_5x", test_img_scale_lanczos_down_0_5x);
917  }