packages feed

flatcv-0.3.1.0: flatcv_impl.c

/* FlatCV - Amalgamated implementation (auto-generated) */
/*
 * ISC License
 * 
 * Copyright (c) 2025 Adrian Sieber
 * 
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
 * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

#define FLATCV_AMALGAMATION
#include "flatcv.h"
// File: src/binary_closing_disk.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif

uint8_t *fcv_binary_dilation_disk(
  uint8_t const *image_data,
  int32_t width,
  int32_t height,
  int32_t radius
) {
  if (!image_data || width <= 0 || height <= 0 || radius < 0) {
    return NULL;
  }

  // Check for overflow: width * height (width and height already validated > 0)
  if ((size_t)width > SIZE_MAX / (size_t)height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * (size_t)height;

  uint8_t *result = malloc(num_pixels);
  if (!result) {
    return NULL;
  }

  // Initialize result to 0 (black)
  memset(result, 0, num_pixels);

  // For each pixel in the image
  for (int32_t y = 0; y < height; y++) {
    for (int32_t x = 0; x < width; x++) {
      int32_t idx = y * width + x;

      // If current pixel is white (255), dilate it
      if (image_data[idx] == 255) {
        // Apply disk-shaped structuring element
        for (int32_t dy = -radius; dy <= radius; dy++) {
          for (int32_t dx = -radius; dx <= radius; dx++) {
            // Check if point32_t is within disk (use slightly larger radius for
            // better connectivity)
            double r_eff = radius + 0.5;
            if (dx * dx + dy * dy <= r_eff * r_eff) {
              int32_t ny = y + dy;
              int32_t nx = x + dx;

              // Check bounds
              if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
                int32_t nidx = ny * width + nx;
                result[nidx] = 255;
              }
            }
          }
        }
      }
    }
  }

  return result;
}

static uint8_t *binary_erosion_disk_internal(
  uint8_t const *image_data,
  int32_t width,
  int32_t height,
  int32_t radius,
  bool replicate_border
) {
  if (!image_data || width <= 0 || height <= 0 || radius < 0) {
    return NULL;
  }

  // Check for overflow: width * height (width and height already validated > 0)
  if ((size_t)width > SIZE_MAX / (size_t)height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * (size_t)height;

  uint8_t *result = malloc(num_pixels);
  if (!result) {
    return NULL;
  }

  // Initialize result to 0 (black)
  memset(result, 0, num_pixels);

  // For each pixel in the image
  for (int32_t y = 0; y < height; y++) {
    for (int32_t x = 0; x < width; x++) {
      int32_t idx = y * width + x;

      // Check if structuring element fits entirely within white pixels
      bool can_erode = true;

      for (int32_t dy = -radius; dy <= radius && can_erode; dy++) {
        for (int32_t dx = -radius; dx <= radius && can_erode; dx++) {
          // Check if point32_t is within disk (use slightly larger radius for
          // better connectivity)
          double r_eff = radius + 0.5;
          if (dx * dx + dy * dy <= r_eff * r_eff) {
            int32_t ny = y + dy;
            int32_t nx = x + dx;

            uint8_t neighbor_value;

            // Check bounds
            if (ny < 0 || ny >= height || nx < 0 || nx >= width) {
              if (replicate_border) {
                // Replicate border: clamp coordinates to valid range
                int32_t clamped_y =
                  ny < 0 ? 0 : (ny >= height ? height - 1 : ny);
                int32_t clamped_x = nx < 0 ? 0 : (nx >= width ? width - 1 : nx);
                neighbor_value = image_data[clamped_y * width + clamped_x];
              }
              else {
                // Default: treat out-of-bounds as black
                neighbor_value = 0;
              }
            }
            else {
              neighbor_value = image_data[ny * width + nx];
            }

            if (neighbor_value != 255) {
              can_erode = false;
            }
          }
        }
      }

      if (can_erode) {
        result[idx] = 255;
      }
    }
  }

  return result;
}

uint8_t *fcv_binary_erosion_disk(
  uint8_t const *image_data,
  int32_t width,
  int32_t height,
  int32_t radius
) {
  // Public API uses default behavior: treat out-of-bounds as black
  return binary_erosion_disk_internal(image_data, width, height, radius, false);
}

uint8_t *fcv_binary_closing_disk(
  uint8_t const *image_data,
  int32_t width,
  int32_t height,
  int32_t radius
) {
  if (!image_data || width <= 0 || height <= 0 || radius < 0) {
    return NULL;
  }

  // Step 1: Dilation
  uint8_t *dilated =
    fcv_binary_dilation_disk(image_data, width, height, radius);
  if (!dilated) {
    return NULL;
  }

  // Step 2: Erosion of the dilated image
  // Use replicate_border=true to preserve original white pixels at borders
  // In a closing operation, out-of-bounds pixels use "replicate" border mode
  // which clamps coordinates to valid range, preventing border pixels from
  // being erroneously eroded due to artificial black boundary
  uint8_t *result =
    binary_erosion_disk_internal(dilated, width, height, radius, true);

  // Free intermediate result
  free(dilated);

  return result;
}

uint8_t *fcv_binary_opening_disk(
  uint8_t const *image_data,
  int32_t width,
  int32_t height,
  int32_t radius
) {
  if (!image_data || width <= 0 || height <= 0 || radius < 0) {
    return NULL;
  }

  // Step 1: Erosion
  uint8_t *eroded = fcv_binary_erosion_disk(image_data, width, height, radius);
  if (!eroded) {
    return NULL;
  }

  // Step 2: Dilation of the eroded image
  uint8_t *result = fcv_binary_dilation_disk(eroded, width, height, radius);

  // Free intermediate result
  free(eroded);

  return result;
}
// File: src/conversion.c
#include <assert.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "draw.h"
#include "parse_hex_color.h"
#include "perspectivetransform.h"
#include "rgba_to_grayscale.h"
#include "single_to_multichannel.h"
#include "sobel_edge_detection.h"
#else
#include "flatcv.h"
#endif

/**
 * Convert raw RGBA row-major top-to-bottom image data
 * to RGBA row-major top-to-bottom grayscale image data.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the pixel data.
 * @return Pointer to the grayscale image data.
 */
uint8_t *
fcv_grayscale(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *grayscale_data = malloc(img_length_byte);

  if (!grayscale_data) { // Memory allocation failed
    return NULL;
  }

  // Process each pixel row by row
  for (uint32_t i = 0; i < width * height; i++) {
    uint32_t rgba_index = i * 4;

    uint8_t r = data[rgba_index];
    uint8_t g = data[rgba_index + 1];
    uint8_t b = data[rgba_index + 2];

    uint8_t gray = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;

    grayscale_data[rgba_index] = gray;
    grayscale_data[rgba_index + 1] = gray;
    grayscale_data[rgba_index + 2] = gray;
    grayscale_data[rgba_index + 3] = 255;
  }

  return grayscale_data;
}

/**
 * Convert raw RGBA row-major top-to-bottom image data
 * to RGBA row-major top-to-bottom grayscale image data
 * with a stretched contrast range.
 * Set the 1.5625 % darkest pixels to 0 and the 1.5625 % brightest to 255.
 * Uses this specific value for speed: x * 1.5625 % = x >> 6
 * The rest of the pixel values are linearly scaled to the range [0, 255].

 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the pixel data.
 * @return Pointer to the grayscale image data.
 */
uint8_t *fcv_grayscale_stretch(
  uint32_t width,
  uint32_t height,
  uint8_t const *const data
) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;
  if (img_length_px > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = img_length_px * 4;

  uint8_t *grayscale_data = malloc(img_length_byte);

  if (!grayscale_data) { // Memory allocation failed
    return NULL;
  }
  // Ignore 1.5625 % of the pixels
  uint32_t num_pixels_to_ignore = img_length_px >> 6;

  uint8_t *gray_values = malloc(img_length_px);
  if (!gray_values) { // Memory allocation failed
    free(grayscale_data);
    return NULL;
  }

  // Process each pixel row by row to get grayscale values
  for (uint32_t i = 0; i < img_length_px; i++) {
    uint32_t rgba_index = i * 4;

    uint8_t r = data[rgba_index];
    uint8_t g = data[rgba_index + 1];
    uint8_t b = data[rgba_index + 2];

    gray_values[i] = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;
  }

  // Use counting sort to find the 1.5625% darkest and brightest pixels
  uint32_t histogram[256] = {0};
  for (uint32_t i = 0; i < img_length_px; i++) {
    histogram[gray_values[i]]++;
  }

  uint32_t cumulative_count = 0;
  uint8_t min_val = 0;
  for (uint32_t i = 0; i < 256; i++) {
    cumulative_count += histogram[i];
    if (cumulative_count > num_pixels_to_ignore) {
      min_val = i;
      break;
    }
  }

  cumulative_count = 0;
  uint8_t max_val = 255;
  for (int32_t i = 255; i >= 0; i--) {
    cumulative_count += histogram[i];
    if (cumulative_count > num_pixels_to_ignore) {
      max_val = i;
      break;
    }
  }

  free(gray_values);

  uint8_t range = max_val - min_val;

  // Process each pixel row by row
  for (size_t i = 0; i < img_length_px; i++) {
    uint32_t rgba_index = i * 4;

    uint8_t r = data[rgba_index];
    uint8_t g = data[rgba_index + 1];
    uint8_t b = data[rgba_index + 2];

    uint8_t gray = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;

    if (range == 0) {
      // All pixels have same value, preserve as-is
      // (no stretching possible)
    }
    else if (gray < min_val) {
      gray = 0;
    }
    else if (gray > max_val) {
      gray = 255;
    }
    else {
      gray = (gray - min_val) * 255 / range;
    }

    grayscale_data[rgba_index] = gray;
    grayscale_data[rgba_index + 1] = gray;
    grayscale_data[rgba_index + 2] = gray;
    grayscale_data[rgba_index + 3] = 255;
  }

  return grayscale_data;
}

/**
 * Apply a global threshold to the image data.
 *
 * @param img_length_px Length of the image data in pixels.
 * @param data Pointer to the image data.
 * @param threshold Threshold value.
 *
 */
void fcv_apply_global_threshold(
  uint32_t img_length_px,
  uint8_t *data,
  uint8_t threshold
) {
  for (uint32_t i = 0; i < img_length_px; i++) {
    data[i] = data[i] > threshold ? 255 : 0;
  }
}

/**
 * Applies two thresholds to the image data by blackening pixels
 * below the lower threshold and whitening pixels above the upper threshold.
 * Pixels between the two thresholds are scaled to the range [0, 255].
 *
 * @param img_length_px Length of the image data in pixels.
 * @param data Pointer to the image data.
 * @param lower_threshold Every pixel below this value will be blackened.
 * @param upper_threshold Every pixel above this value will be whitened.
 *
 */
void apply_double_threshold(
  uint32_t img_length_px,
  uint8_t *data,
  uint8_t lower_threshold,
  uint8_t upper_threshold
) {
  for (uint32_t i = 0; i < img_length_px; i++) {
    if (data[i] < lower_threshold) {
      data[i] = 0;
    }
    else if (data[i] > upper_threshold) {
      data[i] = 255;
    }
    else {
      data[i] =
        (data[i] - lower_threshold) * 255 / (upper_threshold - lower_threshold);
    }
  }
}

/**
 * Apply Otsu's thresholding algorithm to the image data.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param use_double_threshold Whether to use double thresholding.
 * @param data Pointer to the pixel data.
 * @return Pointer to the monochrome image data.
 */
uint8_t *fcv_otsu_threshold_rgba(
  uint32_t width,
  uint32_t height,
  bool use_double_threshold,
  uint8_t const *const data
) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height
  if (width > SIZE_MAX / height) {
    return NULL;
  }

  uint8_t *grayscale_img = fcv_rgba_to_grayscale(width, height, data);
  if (!grayscale_img) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;

  uint32_t histogram[256] = {0};
  for (uint32_t i = 0; i < img_length_px; i++) {
    histogram[grayscale_img[i]]++;
  }

  float histogram_norm[256] = {0};
  for (uint32_t i = 0; i < 256; i++) {
    histogram_norm[i] = (float)histogram[i] / img_length_px;
  }

  float global_mean = 0.0;

  for (uint32_t i = 0; i < 256; i++) {
    global_mean += i * histogram_norm[i];
  }

  float cumulative_sum = 0.0;
  float cumulative_mean = 0.0;
  float max_variance = 0.0;
  int32_t optimal_threshold = 0;

  for (uint32_t i = 0; i < 256; i++) {
    cumulative_sum += histogram_norm[i];
    cumulative_mean += i * histogram_norm[i];

    if (cumulative_sum == 0 || cumulative_sum == 1) {
      continue;
    }

    float mean1 = cumulative_mean / cumulative_sum;
    float mean2 = (global_mean - cumulative_mean) / (1 - cumulative_sum);

    float class_variance =
      cumulative_sum * (1 - cumulative_sum) * (mean1 - mean2) * (mean1 - mean2);

    if (class_variance > max_variance) {
      max_variance = class_variance;
      optimal_threshold = i;
    }
  }

  const int32_t threshold_range_offset = 16;

  if (use_double_threshold) {
    apply_double_threshold(
      img_length_px,
      grayscale_img,
      optimal_threshold - threshold_range_offset,
      optimal_threshold + threshold_range_offset
    );
  }
  else {
    fcv_apply_global_threshold(img_length_px, grayscale_img, optimal_threshold);
  }

  uint8_t *monochrome_data =
    fcv_single_to_multichannel(width, height, grayscale_img);

  free(grayscale_img);

  return monochrome_data;
}

/**
 * Apply gaussian blur to the image data.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the pixel data.
 * @return Pointer to the blurred image data.
 */
uint8_t *fcv_apply_gaussian_blur(
  uint32_t width,
  uint32_t height,
  double radius,
  uint8_t const *const data
) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Validate radius
  if (radius < 0 || !isfinite(radius)) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;
  if (img_length_px > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = img_length_px * 4;

  if (radius == 0) {
    uint8_t *copy = malloc(img_length_byte);
    if (!copy) {
      return NULL;
    }
    return memcpy(copy, data, img_length_byte);
  }

  // Reject excessive radius to prevent excessive memory allocation
  if (radius > 1000) {
    return NULL;
  }

  uint8_t *blurred_data = malloc(img_length_byte);

  if (!blurred_data) { // Memory allocation failed
    return NULL;
  }

  uint32_t kernel_size = (uint32_t)(2 * radius + 1);
  float *kernel = malloc(kernel_size * sizeof(float));

  if (!kernel) { // Memory allocation failed
    free(blurred_data);
    return NULL;
  }

  float sigma = radius / 3.0;
  float sigma_sq = sigma * sigma;
  float two_sigma_sq = 2 * sigma_sq;
  float sqrt_two_pi_sigma = sqrt(2 * M_PI) * sigma;

  for (uint32_t i = 0; i < kernel_size; i++) {
    int32_t x = i - radius;
    kernel[i] = exp(-(x * x) / two_sigma_sq) / sqrt_two_pi_sigma;
  }

  // Apply the kernel in the horizontal direction
  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      float r_sum = 0.0;
      float g_sum = 0.0;
      float b_sum = 0.0;
      float weight_sum = 0.0;

      for (int32_t k = -radius; k <= radius; k++) {
        int32_t x_offset = x + k;
        if (x_offset < 0 || (uint32_t)x_offset >= width) {
          continue;
        }

        uint32_t img_index = y * width + x_offset;
        uint32_t img_rgba_index = img_index * 4;

        // Bounds check for array access
        if (img_rgba_index + 2 >= img_length_px * 4) {
          continue;
        }

        float weight = kernel[k + (int32_t)radius];
        weight_sum += weight;

        r_sum += data[img_rgba_index] * weight;
        g_sum += data[img_rgba_index + 1] * weight;
        b_sum += data[img_rgba_index + 2] * weight;
      }

      uint32_t rgba_index = (y * width + x) * 4;
      blurred_data[rgba_index] = r_sum / weight_sum;
      blurred_data[rgba_index + 1] = g_sum / weight_sum;
      blurred_data[rgba_index + 2] = b_sum / weight_sum;
      blurred_data[rgba_index + 3] = 255;
    }
  }

  // Create temporary buffer for vertical pass to avoid reading from buffer
  // being written to
  uint8_t *temp_data = malloc(img_length_px * 4);
  if (!temp_data) {
    free(blurred_data);
    free(kernel);
    return NULL;
  }

  // Copy horizontal blur result to temp buffer
  memcpy(temp_data, blurred_data, img_length_px * 4);

  // Apply the kernel in the vertical direction
  for (uint32_t x = 0; x < width; x++) {
    for (uint32_t y = 0; y < height; y++) {
      float r_sum = 0.0;
      float g_sum = 0.0;
      float b_sum = 0.0;
      float weight_sum = 0.0;

      for (int32_t k = -radius; k <= radius; k++) {
        int32_t y_offset = y + k;
        if (y_offset < 0 || (uint32_t)y_offset >= height) {
          continue;
        }

        uint32_t img_index = y_offset * width + x;
        uint32_t img_rgba_index = img_index * 4;

        float weight = kernel[k + (int32_t)radius];
        weight_sum += weight;

        r_sum += temp_data[img_rgba_index] * weight;
        g_sum += temp_data[img_rgba_index + 1] * weight;
        b_sum += temp_data[img_rgba_index + 2] * weight;
      }

      uint32_t rgba_index = (y * width + x) * 4;
      blurred_data[rgba_index] = r_sum / weight_sum;
      blurred_data[rgba_index + 1] = g_sum / weight_sum;
      blurred_data[rgba_index + 2] = b_sum / weight_sum;
      blurred_data[rgba_index + 3] = 255;
    }
  }

  free(temp_data);

  free(kernel);

  return blurred_data;
}

#include <time.h>

/**
 * Convert image to anti-aliased black and white.
 * 1. Convert the image to grayscale.
 * 2. Subtract blurred image from the original image to get the high
 * frequencies.
 * 3. Apply OTSU's threshold to get the optimal threshold.
 * 4. Apply the threshold + offset to get the anti-aliased image.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param use_double_threshold Whether to use double thresholding.
 * @param data Pointer to the pixel data.
 * @return Pointer to the blurred image data.
 */
uint8_t *fcv_bw_smart(
  uint32_t width,
  uint32_t height,
  bool use_double_threshold,
  uint8_t const *const data
) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  uint8_t *grayscale_data = fcv_grayscale(width, height, data);
  if (!grayscale_data) {
    return NULL;
  }

  // Calculate blur radius dependent on image size
  // (Empirical formula after testing)
  double blurRadius = (sqrt((double)width * (double)height)) * 0.1;

  uint8_t *blurred_data =
    fcv_apply_gaussian_blur(width, height, blurRadius, grayscale_data);
  if (!blurred_data) {
    free(grayscale_data);
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    free(grayscale_data);
    free(blurred_data);
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;
  if (img_length_px > SIZE_MAX / 4) {
    free(grayscale_data);
    free(blurred_data);
    return NULL;
  }

  uint8_t *high_freq_data = malloc(img_length_px * 4);

  if (!high_freq_data) { // Memory allocation failed
    free((void *)grayscale_data);
    free((void *)blurred_data);
    return NULL;
  }

  // Subtract blurred image from the original image to get the high frequencies
  // and invert the high frequencies to get a white background.
  for (uint32_t i = 0; i < img_length_px; i++) {
    uint32_t rgba_idx = i * 4;
    int32_t high_freq_val =
      127 + grayscale_data[rgba_idx] - blurred_data[rgba_idx];

    // Clamp the value to [0, 255] to prevent overflow
    if (high_freq_val < 0) {
      high_freq_val = 0;
    }
    else if (high_freq_val > 255) {
      high_freq_val = 255;
    }

    high_freq_data[rgba_idx] = high_freq_val;     // R
    high_freq_data[rgba_idx + 1] = high_freq_val; // G
    high_freq_data[rgba_idx + 2] = high_freq_val; // B
    high_freq_data[rgba_idx + 3] = 255;           // A
  }

  free((void *)grayscale_data);
  free((void *)blurred_data);

  uint8_t *final_data = fcv_otsu_threshold_rgba(
    width,
    height,
    use_double_threshold,
    high_freq_data
  );

  free(high_freq_data);

  return final_data;
}

/**
 * Resize an image by given resize factors using bilinear interpolation.
 *
 * @param width Width of the input image.
 * @param height Height of the input image.
 * @param resize_x Horizontal resize factor (e.g., 2.0 for 2x, 0.5 for half).
 * @param resize_y Vertical resize factor (e.g., 2.0 for 2x, 0.5 for half).
 * @param out_width Pointer to store the output image width.
 * @param out_height Pointer to store the output image height.
 * @param data Pointer to the input pixel data.
 * @return Pointer to the resized image data.
 */
uint8_t *fcv_resize(
  uint32_t width,
  uint32_t height,
  double resize_x,
  double resize_y,
  uint32_t *out_width,
  uint32_t *out_height,
  uint8_t const *const data
) {
  if (!data || !out_width || !out_height) {
    return NULL;
  }

  if (width == 0 || height == 0) {
    return NULL;
  }

  if (resize_x <= 0.0 || resize_y <= 0.0 || !isfinite(resize_x) ||
      !isfinite(resize_y)) {
    return NULL;
  }

  // Check for overflow in output dimensions
  double new_width_d = (double)width * resize_x;
  double new_height_d = (double)height * resize_y;

  if (new_width_d > UINT32_MAX || new_height_d > UINT32_MAX) {
    return NULL;
  }

  *out_width = (uint32_t)new_width_d;
  *out_height = (uint32_t)new_height_d;

  if (*out_width == 0 || *out_height == 0) {
    return NULL;
  }

  // Check for overflow: out_width * out_height * 4
  if (*out_width > SIZE_MAX / *out_height) {
    return NULL;
  }
  size_t out_img_pixels = (size_t)(*out_width) * (*out_height);
  if (out_img_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t out_img_length = out_img_pixels * 4;

  uint8_t *resized_data = malloc(out_img_length);

  if (!resized_data) {
    return NULL;
  }

  for (uint32_t out_y = 0; out_y < *out_height; out_y++) {
    for (uint32_t out_x = 0; out_x < *out_width; out_x++) {
      if (resize_x < 1.0 || resize_y < 1.0) {
        double src_x = (out_x + 0.5) / resize_x - 0.5;
        double src_y = (out_y + 0.5) / resize_y - 0.5;

        double filter_size_x = 1.0 / resize_x;
        double filter_size_y = 1.0 / resize_y;

        double x_start = src_x - filter_size_x * 0.5;
        double y_start = src_y - filter_size_y * 0.5;
        double x_end = src_x + filter_size_x * 0.5;
        double y_end = src_y + filter_size_y * 0.5;

        int32_t ix_start = (int32_t)floor(x_start);
        int32_t iy_start = (int32_t)floor(y_start);
        int32_t ix_end = (int32_t)ceil(x_end);
        int32_t iy_end = (int32_t)ceil(y_end);

        if (ix_start < 0) {
          ix_start = 0;
        }
        if (iy_start < 0) {
          iy_start = 0;
        }
        if (ix_end > (int32_t)width) {
          ix_end = width;
        }
        if (iy_end > (int32_t)height) {
          iy_end = height;
        }

        double r_sum = 0.0, g_sum = 0.0, b_sum = 0.0;
        double total_weight = 0.0;

        for (int32_t sy = iy_start; sy < iy_end; sy++) {
          for (int32_t sx = ix_start; sx < ix_end; sx++) {
            double left = sx;
            double right = sx + 1;
            double top = sy;
            double bottom = sy + 1;

            double overlap_left = left > x_start ? left : x_start;
            double overlap_right = right < x_end ? right : x_end;
            double overlap_top = top > y_start ? top : y_start;
            double overlap_bottom = bottom < y_end ? bottom : y_end;

            if (overlap_right > overlap_left && overlap_bottom > overlap_top) {
              double weight =
                (overlap_right - overlap_left) * (overlap_bottom - overlap_top);
              total_weight += weight;

              uint32_t src_idx = (sy * width + sx) * 4;
              r_sum += data[src_idx] * weight;
              g_sum += data[src_idx + 1] * weight;
              b_sum += data[src_idx + 2] * weight;
            }
          }
        }

        if (total_weight > 0.0) {
          resized_data[(out_y * *out_width + out_x) * 4] =
            (uint8_t)(r_sum / total_weight + 0.5);
          resized_data[(out_y * *out_width + out_x) * 4 + 1] =
            (uint8_t)(g_sum / total_weight + 0.5);
          resized_data[(out_y * *out_width + out_x) * 4 + 2] =
            (uint8_t)(b_sum / total_weight + 0.5);
        }
        else {
          resized_data[(out_y * *out_width + out_x) * 4] = 0;
          resized_data[(out_y * *out_width + out_x) * 4 + 1] = 0;
          resized_data[(out_y * *out_width + out_x) * 4 + 2] = 0;
        }
        resized_data[(out_y * *out_width + out_x) * 4 + 3] = 255;
      }
      else {
        double src_x = (out_x + 0.5) / resize_x - 0.5;
        double src_y = (out_y + 0.5) / resize_y - 0.5;

        int32_t x0 = (int32_t)floor(src_x);
        int32_t y0 = (int32_t)floor(src_y);
        int32_t x1 = x0 + 1;
        int32_t y1 = y0 + 1;

        if (x0 < 0) {
          x0 = 0;
        }
        if (y0 < 0) {
          y0 = 0;
        }
        if (x1 >= (int32_t)width) {
          x1 = width - 1;
        }
        if (y1 >= (int32_t)height) {
          y1 = height - 1;
        }

        double dx = src_x - x0;
        double dy = src_y - y0;

        if (dx < 0) {
          dx = 0;
        }
        if (dy < 0) {
          dy = 0;
        }
        if (dx > 1) {
          dx = 1;
        }
        if (dy > 1) {
          dy = 1;
        }

        for (int32_t c = 0; c < 3; c++) {
          uint8_t p00 = data[(y0 * width + x0) * 4 + c];
          uint8_t p01 = data[(y0 * width + x1) * 4 + c];
          uint8_t p10 = data[(y1 * width + x0) * 4 + c];
          uint8_t p11 = data[(y1 * width + x1) * 4 + c];

          double interpolated = p00 * (1 - dx) * (1 - dy) +
                                p01 * dx * (1 - dy) + p10 * (1 - dx) * dy +
                                p11 * dx * dy;

          resized_data[(out_y * *out_width + out_x) * 4 + c] =
            (uint8_t)(interpolated + 0.5);
        }

        resized_data[(out_y * *out_width + out_x) * 4 + 3] = 255;
      }
    }
  }

  return resized_data;
}
// File: src/convert_to_binary.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "convert_to_binary.h"
#include "parse_hex_color.h"
#else
#include "flatcv.h"
#endif

/**
 * Convert an image to binary format based on foreground and background colors.
 * Return a grayscale image where the foreground color is white and the
 * background color is black.
 *
 * @param image_data Pointer to the input image data (RGBA format).
 * @param width Width of the image.
 * @param height Height of the image.
 * @param foreground_hex Hex color code for the foreground color (e.g.,
 * "FF0000").
 * @param background_hex Hex color code for the background color (e.g.,
 * "00FF00").
 */
uint8_t *fcv_convert_to_binary(
  const uint8_t *image_data,
  int32_t width,
  int32_t height,
  const char *foreground_hex,
  const char *background_hex
) {
  if (!image_data || !foreground_hex || !background_hex) {
    return NULL;
  }

  if (width <= 0 || height <= 0) {
    return NULL;
  }

  // Check for overflow: width * height (width and height already validated > 0)
  if ((size_t)width > SIZE_MAX / (size_t)height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * (size_t)height;

  uint8_t *result = malloc(num_pixels);
  if (!result) {
    return NULL;
  }

  uint8_t r_fg, g_fg, b_fg;
  fcv_parse_hex_color(foreground_hex, &r_fg, &g_fg, &b_fg);

  uint8_t r_bg, g_bg, b_bg;
  fcv_parse_hex_color(background_hex, &r_bg, &g_bg, &b_bg);

  for (size_t i = 0; i < num_pixels; i++) {
    size_t rgba_index = i * 4;
    uint8_t r = image_data[rgba_index];
    uint8_t g = image_data[rgba_index + 1];
    uint8_t b = image_data[rgba_index + 2];

    if (r == r_fg && g == g_fg && b == b_fg) {
      // Convert foreground color to white
      result[i] = 255;
    }
    else if (r == r_bg && g == g_bg && b == b_bg) {
      // Convert background color to black
      result[i] = 0;
    }
    else {
      // Default to black for unmatched colors
      result[i] = 0;
    }
  }

  return result;
}
// File: src/corner_detection.c
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "binary_closing_disk.h"
#include "conversion.h"
#include "convert_to_binary.h"
#include "corner_peaks.h"
#include "draw.h"
#include "foerstner_corner.h"
#include "parse_hex_color.h"
#include "perspectivetransform.h"
#include "sobel_edge_detection.h"
#include "sort_corners.h"
#include "watershed_segmentation.h"

#ifdef DEBUG_LOGGING
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#endif
#else
#include "flatcv.h"
#endif

/** Count number of distinct colors in RGBA image
 * 1. Convert to grayscale
 * 2. Scale to 256x256 (save scale ratio for x and y)
 * 3. Blur image
 * 4. Create elevation map with Sobel
 * 5. Flatten elevation map at center seed to avoid being trapped in a local
 * minimum
 * 6. Add black border to flood from all directions at once
 * 7. `watershed(image=elevation_map, markers=markers)`
 *     Set center as marker for foreground basin and border for background basin
 * 8. Check region count equals 2
 * 9. Smooth result with binary closing
 * 10. Use Foerstner corner detector
 *     (Harris detector corners are shifted inwards)
 * 11. Sort corners
 * 12. Select 4 corners with the largest angle while maintaining their order
 * 13. Normalize corners based on scale ratio
 */
int32_t count_colors(const uint8_t *image, int32_t width, int32_t height) {
  assert(image != NULL);
  assert(width > 0);
  assert(height > 0);

  int32_t color_count = 0;
  int32_t *color_map = (int32_t *)calloc(256 * 256 * 256, sizeof(int32_t));
  if (!color_map) {
    fprintf(stderr, "Error: Failed to allocate memory for color map\n");
    return -1;
  }

  for (int32_t i = 0; i < width * height * 4; i += 4) {
    uint8_t r = image[i];
    uint8_t g = image[i + 1];
    uint8_t b = image[i + 2];
    uint8_t a = image[i + 3];

    // Skip fully transparent pixels
    if (a == 0) {
      continue;
    }

    // Create a unique color key
    int32_t color_key = (r << 16) | (g << 8) | b;

    // Increment the count for this color
    if (color_map[color_key] == 0) {
      color_count++;
    }
    color_map[color_key]++;
  }

  free(color_map);
  return color_count;
}

typedef struct {
  int32_t width;
  int32_t height;
  int32_t channels;
  const uint8_t *data;
} Image;

#ifdef DEBUG_LOGGING
void write_debug_img(Image img, const char *filename) {
  int32_t result = stbi_write_png(
    filename,
    img.width,
    img.height,
    img.channels,
    img.data,
    img.width * img.channels
  );
  if (result == 0) {
    fprintf(stderr, "Error: Failed to write debug image %s\n", filename);
  }
}
#endif

/**
 * Detect corners in the input image.
 */
Corners
fcv_detect_corners(const uint8_t *image, int32_t width, int32_t height) {
  assert(image != NULL);
  assert(width > 0);
  assert(height > 0);

  Corners default_corners = {
    .tl_x = 0,
    .tl_y = 0,
    .tr_x = width - 1,
    .tr_y = 0,
    .br_x = width - 1,
    .br_y = height - 1,
    .bl_x = 0,
    .bl_y = height - 1
  };

  // 1. Convert to grayscale
  uint8_t const *grayscale_image = fcv_grayscale(width, height, image);
  if (!grayscale_image) {
    fprintf(stderr, "Error: Failed to convert image to grayscale\n");
    return default_corners;
  }

  // 2. Resize image to 256x256
  uint32_t out_width = 256;
  uint32_t out_height = 256;
  uint8_t const *resized_image = fcv_resize(
    width,
    height,
    (double)out_width / width,
    (double)out_height / height,
    &out_width,
    &out_height,
    grayscale_image
  );
  free((void *)grayscale_image);
  if (!resized_image) {
    fprintf(stderr, "Error: Failed to resize image\n");
    exit(EXIT_FAILURE);
  }

#ifdef DEBUG_LOGGING
  Image out_img = {
    .width = out_width,
    .height = out_height,
    .channels = 4,
    .data = resized_image
  };
  write_debug_img(out_img, "temp_1_resized_image.png");
#endif

  // 3. Apply Gaussian blur
  uint8_t const *blurred_image =
    fcv_apply_gaussian_blur(out_width, out_height, 3.0, resized_image);
#ifndef DEBUG_LOGGING
  free((void *)resized_image);
#endif
  if (!blurred_image) {
    fprintf(stderr, "Error: Failed to apply Gaussian blur\n");
    exit(EXIT_FAILURE);
  }

  // 4. Create elevation map with Sobel edge detection
  uint8_t *elevation_map = (uint8_t *)
    fcv_sobel_edge_detection(out_width, out_height, 4, blurred_image);
  free((void *)blurred_image);
  if (!elevation_map) {
    fprintf(stderr, "Error: Failed to create elevation map with Sobel\n");
    exit(EXIT_FAILURE);
  }

  // 5. Flatten elevation map at center to not get trapped in a local minimum
  fcv_draw_disk(
    out_width,
    out_height,
    1, // Single channel grayscale
    "000000",
    24,
    out_width / 2.0,
    out_height / 2.0,
    elevation_map
  );

  // 6. Add black border to flood from all directions at once
  uint32_t bordered_width, bordered_height;
  uint8_t *bordered_elevation_map = fcv_add_border(
    out_width,
    out_height,
    1,        // Single channel grayscale
    "000000", // Black border
    1,        // Border width of 1 pixel
    elevation_map,
    &bordered_width,
    &bordered_height
  );

  free(elevation_map);
  if (!bordered_elevation_map) {
    fprintf(stderr, "Error: Failed to add black border to elevation map\n");
    exit(EXIT_FAILURE);
  }

#ifdef DEBUG_LOGGING
  out_img.data = bordered_elevation_map;
  out_img.channels = 1; // Grayscale
  out_img.width = bordered_width;
  out_img.height = bordered_height;
  write_debug_img(out_img, "temp_2_elevation_map.png");
#endif

  // 7. Perform watershed segmentation
  int32_t num_markers = 2;
  Point2D *markers = malloc(num_markers * sizeof(Point2D));
  if (!markers) {
    fprintf(stderr, "Error: Failed to allocate memory for markers\n");
    free((void *)bordered_elevation_map);
    exit(EXIT_FAILURE);
  }
  // Set center as foreground marker and upper left corner as background marker
  // Adjust for the added border (border width = 1)
  markers[0] = (Point2D){.x = bordered_width / 2.0, .y = bordered_height / 2.0};
  markers[1] = (Point2D){.x = 0, .y = 0};

  uint8_t *segmented_image_wide = fcv_watershed_segmentation(
    bordered_width,
    bordered_height,
    bordered_elevation_map,
    markers,
    num_markers,
    false // No boundaries
  );
  free((void *)bordered_elevation_map);
  free((void *)markers);

  // Remove 1 pixel border from segmented image
  uint8_t *segmented_image = malloc(out_width * out_height * 4);
  if (!segmented_image) {
    fprintf(stderr, "Error: Failed to allocate memory for segmented image\n");
    free((void *)segmented_image_wide);
    exit(EXIT_FAILURE);
  }
  for (uint32_t y = 1; y < bordered_height - 1; y++) {
    memcpy(
      &segmented_image[(y - 1) * out_width * 4],
      &segmented_image_wide[(y * bordered_width + 1) * 4],
      out_width * 4
    );
  }

#ifdef DEBUG_LOGGING
  out_img.width = out_width;
  out_img.height = out_height;
  out_img.channels = 4;
  out_img.data = segmented_image;
  write_debug_img(out_img, "temp_3_watershed.png");
#endif

  // 8. Check if the image has exactly 2 regions (foreground and background)
  int32_t region_count = count_colors(segmented_image, out_width, out_height);
  if (region_count != 2) {
    fprintf(stderr, "Error: Expected 2 regions, found %d\n", region_count);
    free((void *)segmented_image);
    exit(EXIT_FAILURE);
  }

  // Convert red pixel (marker 1 - foreground) to white
  // and green pixel (marker 2 - background) to black
  uint8_t *segmented_binary = fcv_convert_to_binary(
    segmented_image,
    out_width,
    out_height,
    "FF0000", // red -> white
    "00FF00"  // green -> black
  );
  free((void *)segmented_image);

#ifdef DEBUG_LOGGING
  out_img.data = segmented_binary;
  out_img.channels = 1;
  write_debug_img(out_img, "temp_4_watershed_binary.png");
#endif

  // 9. Smooth the result
  uint8_t *segmented_closed = fcv_binary_closing_disk(
    segmented_binary,
    out_width,
    out_height,
    12 // Closing radius
  );
  free((void *)segmented_binary);
  if (!segmented_closed) {
    fprintf(stderr, "Error: Failed to perform binary closing\n");
    exit(EXIT_FAILURE);
  }

#ifdef DEBUG_LOGGING
  out_img.data = segmented_closed;
  write_debug_img(out_img, "temp_5_segmented_closed.png");
#endif

  // 10. Find corners in the closed image
  uint8_t *corner_response = fcv_foerstner_corner(
    out_width,
    out_height,
    segmented_closed,
    1.5 // Sigma for Gaussian smoothing
  );
  free((void *)segmented_closed);
  if (!corner_response) {
    fprintf(stderr, "Error: Failed to compute corner response\n");
    exit(EXIT_FAILURE);
  }

  // Extract `w` channel (error ellipse size) for visualization
  uint8_t *w_channel = malloc(out_width * out_height);
  if (!w_channel) {
    fprintf(stderr, "Error: Failed to allocate memory for w channel\n");
    free((void *)corner_response);
    exit(EXIT_FAILURE);
  }

  for (uint32_t i = 0; i < out_width * out_height; i++) {
    w_channel[i] = corner_response[i * 2]; // Extract `w` channel
  }

#ifdef DEBUG_LOGGING
  out_img.data = w_channel;
  write_debug_img(out_img, "temp_6_corner_response.png");
#endif
  free(w_channel);

  // 10. Find corner peaks using thresholds
  // Gradually decrease thresholds until it finds at least 4 corners
  CornerPeaks *peaks = NULL;
  double accuracy_thresh = 0.4;
  double roundness_thresh = 0.2;
  const double thresh_decrement = 0.05;
  const double min_thresh = 0.01;

  do {
    if (peaks) {
      free(peaks->points);
      free(peaks);
    }

    peaks = fcv_corner_peaks(
      out_width,
      out_height,
      corner_response,
      16, // Minimum distance between peaks
      accuracy_thresh,
      roundness_thresh
    );

    if (!peaks || peaks->count < 4) {
      accuracy_thresh -= thresh_decrement;
      roundness_thresh -= thresh_decrement;

      // Prevent thresholds from going negative
      if (accuracy_thresh < min_thresh) {
        accuracy_thresh = min_thresh;
      }
      if (roundness_thresh < min_thresh) {
        roundness_thresh = min_thresh;
      }
    }

  } while (peaks && peaks->count < 4 &&
           (accuracy_thresh > min_thresh || roundness_thresh > min_thresh));
  free((void *)corner_response);
  if (!peaks) {
    fprintf(stderr, "Error: Failed to find corner peaks\n");
    exit(EXIT_FAILURE);
  }

  // First, sort all corners to get them in clockwise order
  Point2D *sorted_result = malloc(peaks->count * sizeof(Point2D));
  sort_corners(
    width,
    height,
    out_width,
    out_height,
    peaks->points,
    peaks->count,
    sorted_result
  );

  // Scale corners back to original image dimensions
  double scale_x = (double)width / out_width;
  double scale_y = (double)height / out_height;

  Corners sorted_corners;

  if (peaks->count > 4) {
    // Calculate angles for each corner using cross product method
    double *angles = malloc(peaks->count * sizeof(double));
    typedef struct {
      uint32_t index;
      double angle_abs;
    } AngleIndex;
    AngleIndex *angle_indices = malloc(peaks->count * sizeof(AngleIndex));

    for (uint32_t i = 0; i < peaks->count; i++) {
      // Get the three consecutive points for angle calculation
      Point2D prev = sorted_result[(i - 1 + peaks->count) % peaks->count];
      Point2D curr = sorted_result[i];
      Point2D next = sorted_result[(i + 1) % peaks->count];

      // Calculate vectors from current point
      double a_x = prev.x - curr.x;
      double a_y = prev.y - curr.y;
      double b_x = next.x - curr.x;
      double b_y = next.y - curr.y;

      // Calculate lengths of vectors
      double a_len = sqrt(a_x * a_x + a_y * a_y);
      double b_len = sqrt(b_x * b_x + b_y * b_y);

      if (a_len == 0 || b_len == 0) {
        angles[i] = 0;
      }
      else {
        // Calculate cross product (z-component in 2D)
        double cross_product = (a_x * b_y - a_y * b_x) / (a_len * b_len);
        // Clamp to [-1, 1] to avoid numerical errors
        if (cross_product > 1.0) {
          cross_product = 1.0;
        }
        if (cross_product < -1.0) {
          cross_product = -1.0;
        }

        // Calculate angle in degrees
        angles[i] = asin(cross_product) * 180.0 / M_PI;
      }

      angle_indices[i].index = i;
      angle_indices[i].angle_abs = fabs(angles[i]);
    }

    // Sort indices by descending angle magnitude
    for (uint32_t i = 0; i < peaks->count - 1; i++) {
      for (uint32_t j = 0; j < peaks->count - 1 - i; j++) {
        if (angle_indices[j].angle_abs < angle_indices[j + 1].angle_abs) {
          AngleIndex temp = angle_indices[j];
          angle_indices[j] = angle_indices[j + 1];
          angle_indices[j + 1] = temp;
        }
      }
    }

    // Take top 4 indices and sort them to maintain original clockwise order
    uint32_t top_4_indices[4];
    for (uint32_t i = 0; i < 4; i++) {
      top_4_indices[i] = angle_indices[i].index;
    }

    // Sort the top 4 indices to maintain original clockwise order
    for (uint32_t i = 0; i < 3; i++) {
      for (uint32_t j = 0; j < 3 - i; j++) {
        if (top_4_indices[j] > top_4_indices[j + 1]) {
          uint32_t temp = top_4_indices[j];
          top_4_indices[j] = top_4_indices[j + 1];
          top_4_indices[j + 1] = temp;
        }
      }
    }

    // Create final corners struct directly from selected corners in clockwise
    // order The first sorted corner becomes top-left, and we maintain clockwise
    // sequence
    sorted_corners =
      (Corners){.tl_x = sorted_result[top_4_indices[0]].x * scale_x,
                .tl_y = sorted_result[top_4_indices[0]].y * scale_y,
                .tr_x = sorted_result[top_4_indices[1]].x * scale_x,
                .tr_y = sorted_result[top_4_indices[1]].y * scale_y,
                .br_x = sorted_result[top_4_indices[2]].x * scale_x,
                .br_y = sorted_result[top_4_indices[2]].y * scale_y,
                .bl_x = sorted_result[top_4_indices[3]].x * scale_x,
                .bl_y = sorted_result[top_4_indices[3]].y * scale_y};

    free(angles);
    free(angle_indices);
  }
  else {
    // Use all available corners if we have 4 or fewer, already in clockwise
    // order
    sorted_corners =
      (Corners){.tl_x = sorted_result[0].x * scale_x,
                .tl_y = sorted_result[0].y * scale_y,
                .tr_x = sorted_result[1 % peaks->count].x * scale_x,
                .tr_y = sorted_result[1 % peaks->count].y * scale_y,
                .br_x = sorted_result[2 % peaks->count].x * scale_x,
                .br_y = sorted_result[2 % peaks->count].y * scale_y,
                .bl_x = sorted_result[3 % peaks->count].x * scale_x,
                .bl_y = sorted_result[3 % peaks->count].y * scale_y};
  }

  // Selecting the strongest four peaks can discard the starting corner and
  // change the document bounds. Label the final quadrilateral again rather
  // than inheriting the first surviving peak as top-left.
  Point2D selected[4] = {
    {sorted_corners.tl_x, sorted_corners.tl_y},
    {sorted_corners.tr_x, sorted_corners.tr_y},
    {sorted_corners.br_x, sorted_corners.br_y},
    {sorted_corners.bl_x, sorted_corners.bl_y}
  };
  Point2D ordered[4];
  sorted_corners =
    sort_corners(width, height, width, height, selected, 4, ordered);

  free(sorted_result);

#ifdef DEBUG_LOGGING
  // Print peaks for debugging
  fprintf(stderr, "Peaks:\n");
  fprintf(stderr, "%.0f,%.0f\n", sorted_corners.tl_x, sorted_corners.tl_y);
  fprintf(stderr, "%.0f,%.0f\n", sorted_corners.tr_x, sorted_corners.tr_y);
  fprintf(stderr, "%.0f,%.0f\n", sorted_corners.br_x, sorted_corners.br_y);
  fprintf(stderr, "%.0f,%.0f\n", sorted_corners.bl_x, sorted_corners.bl_y);
  // Draw corner peaks on the grayscale and resized image
  for (uint32_t i = 0; i < peaks->count; i++) {
    // Print peak coordinates for debugging
    fprintf(
      stderr,
      "Peak %u: (%.1f, %.1f)\n",
      i,
      peaks->points[i].x,
      peaks->points[i].y
    );
    fcv_draw_circle(
      out_width,
      out_height,
      4,        // GRAY as RGBA (each value is the same)
      "FFFF00", // Yellow color for corners
      2,        // Radius
      peaks->points[i].x,
      peaks->points[i].y,
      (uint8_t *)resized_image
    );
  }

  // Draw sorted corners with different colors to distinguish from peaks.
  // Reuse the scale variables from above.
  // (Inverted for drawing on resized image.)
  double draw_scale_x = (double)out_width / width;
  double draw_scale_y = (double)out_height / height;

  // Draw top-left corner in red
  fcv_draw_circle(
    out_width,
    out_height,
    4,
    "FF0000", // Red color for top-left
    3,        // Slightly larger radius to distinguish from peaks
    sorted_corners.tl_x * draw_scale_x,
    sorted_corners.tl_y * draw_scale_y,
    (uint8_t *)resized_image
  );

  // Draw top-right corner in green
  fcv_draw_circle(
    out_width,
    out_height,
    4,
    "00FF00", // Green color for top-right
    3,
    sorted_corners.tr_x * draw_scale_x,
    sorted_corners.tr_y * draw_scale_y,
    (uint8_t *)resized_image
  );

  // Draw bottom-right corner in blue
  fcv_draw_circle(
    out_width,
    out_height,
    4,
    "0000FF", // Blue color for bottom-right
    3,
    sorted_corners.br_x * draw_scale_x,
    sorted_corners.br_y * draw_scale_y,
    (uint8_t *)resized_image
  );

  // Draw bottom-left corner in magenta
  fcv_draw_circle(
    out_width,
    out_height,
    4,
    "FF00FF", // Magenta color for bottom-left
    3,
    sorted_corners.bl_x * draw_scale_x,
    sorted_corners.bl_y * draw_scale_y,
    (uint8_t *)resized_image
  );

  Image corners_img = {
    .width = out_width,
    .height = out_height,
    .channels = 4,
    .data = resized_image
  };
  write_debug_img(corners_img, "temp_7_corners.png");
  free((void *)resized_image);
#endif

  free(peaks);
  return sorted_corners;
}

/**
 * Wrapper function that allocates corners on heap for FFI.
 */
Corners *
fcv_detect_corners_ptr(const uint8_t *image, int32_t width, int32_t height) {
  Corners corners = fcv_detect_corners(image, width, height);
  Corners *result = malloc(sizeof(Corners));
  if (result == NULL) {
    return NULL;
  }
  *result = corners;
  return result;
}
// File: src/corner_peaks.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#include "conversion.h"
#include "corner_peaks.h"
#else
#include "flatcv.h"
#endif

static bool is_local_maximum(
  uint8_t const *data,
  uint32_t width,
  uint32_t height,
  uint32_t x,
  uint32_t y,
  uint32_t channel
) {
  if (x == 0 || y == 0 || x >= width - 1 || y >= height - 1) {
    return false;
  }

  uint32_t center_idx = (y * width + x) * 2 + channel;
  uint8_t center_val = data[center_idx];

  if (center_val == 0) {
    return false;
  }

  for (int32_t dy = -1; dy <= 1; dy++) {
    for (int32_t dx = -1; dx <= 1; dx++) {
      if (dx == 0 && dy == 0) {
        continue;
      }

      uint32_t neighbor_idx = ((y + dy) * width + (x + dx)) * 2 + channel;
      if (data[neighbor_idx] > center_val) {
        return false;
      }
    }
  }

  return true;
}

static double euclidean_distance(Point2D a, Point2D b) {
  double dx = a.x - b.x;
  double dy = a.y - b.y;
  return sqrt(dx * dx + dy * dy);
}

/** Detect corner peaks in the image.
 * This function finds local maxima in the corner response image
 * and filters them based on specified thresholds.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the 2 channel (w and q) corner response data.
 * @param min_distance Minimum distance between peaks.
 * @param accuracy_thresh Threshold for accuracy measure (w).
 * @param roundness_thresh Threshold for roundness measure (q).
 * @return Pointer to CornerPeaks structure containing detected peaks.
 */
CornerPeaks *fcv_corner_peaks(
  uint32_t width,
  uint32_t height,
  uint8_t const *data,
  uint32_t min_distance,
  double accuracy_thresh,
  double roundness_thresh
) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Validate thresholds
  if (!isfinite(accuracy_thresh) || !isfinite(roundness_thresh)) {
    return NULL;
  }

  // Check for overflow: width * height
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;

  // Check for overflow in candidates allocation
  if (num_pixels > SIZE_MAX / sizeof(Point2D)) {
    return NULL;
  }

  Point2D *candidates = malloc(sizeof(Point2D) * num_pixels);
  if (!candidates) {
    return NULL;
  }

  uint32_t candidate_count = 0;

  for (uint32_t y = 1; y < height - 1; y++) {
    for (uint32_t x = 1; x < width - 1; x++) {
      uint32_t idx = (y * width + x) * 2;

      // Convert normalized values (0-255) back to 0-1 range
      double w = data[idx] / 255.0;     // accuracy measure
      double q = data[idx + 1] / 255.0; // roundness measure

      if (q > roundness_thresh && w > accuracy_thresh &&
          is_local_maximum(data, width, height, x, y, 0)) {
        candidates[candidate_count].x = (double)x;
        candidates[candidate_count].y = (double)y;
        candidate_count++;
      }
    }
  }

  if (candidate_count == 0) {
    free(candidates);
    CornerPeaks *result = malloc(sizeof(CornerPeaks));
    if (result) {
      result->points = NULL;
      result->count = 0;
    }
    return result;
  }

  bool *rejected = calloc(candidate_count, sizeof(bool));
  if (!rejected) {
    free(candidates);
    return NULL;
  }

  for (uint32_t i = 0; i < candidate_count; i++) {
    if (rejected[i]) {
      continue;
    }

    for (uint32_t j = i + 1; j < candidate_count; j++) {
      if (rejected[j]) {
        continue;
      }

      if (euclidean_distance(candidates[i], candidates[j]) < min_distance) {
        uint32_t i_idx =
          ((uint32_t)candidates[i].y * width + (uint32_t)candidates[i].x) * 2;
        uint32_t j_idx =
          ((uint32_t)candidates[j].y * width + (uint32_t)candidates[j].x) * 2;

        if (data[i_idx] >= data[j_idx]) {
          rejected[j] = true;
        }
        else {
          rejected[i] = true;
          break;
        }
      }
    }
  }

  uint32_t final_count = 0;
  for (uint32_t i = 0; i < candidate_count; i++) {
    if (!rejected[i]) {
      final_count++;
    }
  }

  Point2D *final_points = malloc(sizeof(Point2D) * final_count);
  if (!final_points) {
    free(candidates);
    free(rejected);
    return NULL;
  }

  uint32_t idx = 0;
  for (uint32_t i = 0; i < candidate_count; i++) {
    if (!rejected[i]) {
      final_points[idx] = candidates[i];
      idx++;
    }
  }

  free(candidates);
  free(rejected);

  CornerPeaks *result = malloc(sizeof(CornerPeaks));
  if (!result) {
    free(final_points);
    return NULL;
  }

  result->points = final_points;
  result->count = final_count;

  return result;
}
// File: src/crop.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "crop.h"
#else
#include "flatcv.h"
#endif

/**
 * Crop an image.
 *
 * @param width Width of the original image.
 * @param height Height of the original image.
 * @param channels Number of channels in the image.
 * @param data Pointer to the pixel data.
 * @param x The x-coordinate of the top-left corner of the crop area.
 * @param y The y-coordinate of the top-left corner of the crop area.
 * @param new_width The width of the crop area.
 * @param new_height The height of the crop area.
 * @return Pointer to the new cropped image data.
 */
uint8_t *fcv_crop(
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  uint8_t const *const data,
  uint32_t x,
  uint32_t y,
  uint32_t new_width,
  uint32_t new_height
) {
  if (!data) {
    return NULL;
  }

  if (width == 0 || height == 0 || channels == 0 || new_width == 0 ||
      new_height == 0) {
    return NULL;
  }

  // Check for overflow in x + new_width and y + new_height
  if (x > UINT32_MAX - new_width || y > UINT32_MAX - new_height) {
    return NULL;
  }

  if (x + new_width > width || y + new_height > height) {
    fprintf(stderr, "Crop area is outside the original image bounds.\n");
    return NULL;
  }

  // Check for overflow in memory allocation: new_width * new_height * channels
  if (new_width > SIZE_MAX / new_height) {
    return NULL;
  }
  size_t num_pixels = (size_t)new_width * new_height;
  if (num_pixels > SIZE_MAX / channels) {
    return NULL;
  }
  size_t alloc_size = num_pixels * channels;

  uint8_t *cropped_data = malloc(alloc_size);
  if (!cropped_data) {
    fprintf(stderr, "Memory allocation failed for cropped image.\n");
    return NULL;
  }

  size_t row_bytes = (size_t)new_width * channels;
  for (uint32_t i = 0; i < new_height; ++i) {
    size_t src_index = ((size_t)(y + i) * width + x) * channels;
    size_t dst_index = (size_t)i * row_bytes;
    memcpy(cropped_data + dst_index, data + src_index, row_bytes);
  }

  return cropped_data;
}
// File: src/draw.c
#include <assert.h>
#include <limits.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "draw.h"
#include "parse_hex_color.h"
#else
#include "flatcv.h"
#endif

/**
 * Helper function to set a pixel to specified color (for circle drawing)
 */
void fcv_set_circle_pixel(
  uint8_t *data,
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  int32_t px,
  int32_t py,
  uint8_t r,
  uint8_t g,
  uint8_t b
) {
  if (!data || width == 0 || height == 0 || channels == 0) {
    return;
  }
  if (px >= 0 && px < (int32_t)width && py >= 0 && py < (int32_t)height) {
    size_t pixel_index = ((size_t)py * width + px) * channels;

    if (channels == 1) {
      // Grayscale: use luminance formula
      data[pixel_index] = (uint8_t)(0.299 * r + 0.587 * g + 0.114 * b);
    }
    else if (channels >= 3) {
      data[pixel_index] = r;     // R
      data[pixel_index + 1] = g; // G
      data[pixel_index + 2] = b; // B
      if (channels == 4) {
        data[pixel_index + 3] = 255; // A
      }
    }
  }
}

/**
 * Helper function to draw 8 symmetric points of a circle
 */
void fcv_draw_circle_points(
  uint8_t *data,
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  int32_t cx,
  int32_t cy,
  int32_t x,
  int32_t y,
  uint8_t r,
  uint8_t g,
  uint8_t b
) {
  fcv_set_circle_pixel(data, width, height, channels, cx + x, cy + y, r, g, b);
  fcv_set_circle_pixel(data, width, height, channels, cx - x, cy + y, r, g, b);
  fcv_set_circle_pixel(data, width, height, channels, cx + x, cy - y, r, g, b);
  fcv_set_circle_pixel(data, width, height, channels, cx - x, cy - y, r, g, b);
  fcv_set_circle_pixel(data, width, height, channels, cx + y, cy + x, r, g, b);
  fcv_set_circle_pixel(data, width, height, channels, cx - y, cy + x, r, g, b);
  fcv_set_circle_pixel(data, width, height, channels, cx + y, cy - x, r, g, b);
  fcv_set_circle_pixel(data, width, height, channels, cx - y, cy - x, r, g, b);
}

/**
 * Helper function to fill horizontal lines for a filled circle (disk)
 */
void fcv_fill_disk_lines(
  uint8_t *data,
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  int32_t cx,
  int32_t cy,
  int32_t x,
  int32_t y,
  uint8_t r,
  uint8_t g,
  uint8_t b
) {
  // Fill horizontal lines at y offsets
  for (int32_t i = cx - x; i <= cx + x; i++) {
    fcv_set_circle_pixel(data, width, height, channels, i, cy + y, r, g, b);
    fcv_set_circle_pixel(data, width, height, channels, i, cy - y, r, g, b);
  }

  // Fill horizontal lines at x offsets
  // (avoid duplicating center line when x == y)
  if (x != y) {
    for (int32_t i = cx - y; i <= cx + y; i++) {
      fcv_set_circle_pixel(data, width, height, channels, i, cy + x, r, g, b);
      fcv_set_circle_pixel(data, width, height, channels, i, cy - x, r, g, b);
    }
  }
}

/**
 * Draw a circle on an image using Bresenham's circle algorithm
 * (modifies in-place).
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param channels Number of channels in the image (1, 3, or 4).
 * @param hex_color Hex color code (e.g., "FF0000" for red, "#00FF00" for
 * green).
 * @param radius Radius of the circle.
 * @param center_x X coordinate of the circle center.
 * @param center_y Y coordinate of the circle center.
 * @param data Pointer to the pixel data (modified in-place).
 */
void fcv_draw_circle(
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  const char *hex_color,
  double radius,
  double center_x,
  double center_y,
  uint8_t *data
) {
  if (!data || !hex_color || width == 0 || height == 0 || channels == 0) {
    return;
  }

  // Validate radius and center coordinates
  if (radius < 0 || !isfinite(radius) || !isfinite(center_x) ||
      !isfinite(center_y)) {
    return;
  }

  // Reject excessive radius to prevent integer overflow in Bresenham algorithm
  if (radius > INT32_MAX / 2) {
    return;
  }

  // Parse color
  uint8_t r, g, b;
  fcv_parse_hex_color(hex_color, &r, &g, &b);

  int32_t radius_int32_t = (int32_t)radius;
  int32_t cx = (int32_t)center_x;
  int32_t cy = (int32_t)center_y;

  // Bresenham's circle algorithm
  int32_t x = 0;
  int32_t y = radius_int32_t;
  int32_t d = 3 - 2 * radius_int32_t;

  // Initial points
  fcv_draw_circle_points(data, width, height, channels, cx, cy, x, y, r, g, b);

  while (y >= x) {
    x++;
    if (d > 0) {
      y--;
      d = d + 4 * (x - y) + 10;
    }
    else {
      d = d + 4 * x + 6;
    }
    fcv_draw_circle_points(
      data,
      width,
      height,
      channels,
      cx,
      cy,
      x,
      y,
      r,
      g,
      b
    );
  }
}

/**
 * Draw a filled circle (disk) on an image using Bresenham's circle
 * algorithm (modifies in-place).
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param channels Number of channels in the image (1, 3, or 4).
 * @param hex_color Hex color code (e.g., "FF0000" for red, "#00FF00" for
 * green).
 * @param radius Radius of the disk.
 * @param center_x X coordinate of the disk center.
 * @param center_y Y coordinate of the disk center.
 * @param data Pointer to the pixel data (modified in-place).
 */
void fcv_draw_disk(
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  const char *hex_color,
  double radius,
  double center_x,
  double center_y,
  uint8_t *data
) {
  if (!data || !hex_color || width == 0 || height == 0 || channels == 0) {
    return;
  }

  // Validate radius and center coordinates
  if (radius < 0 || !isfinite(radius) || !isfinite(center_x) ||
      !isfinite(center_y)) {
    return;
  }

  // Reject excessive radius to prevent integer overflow in Bresenham algorithm
  if (radius > INT32_MAX / 2) {
    return;
  }

  // Parse color
  uint8_t r, g, b;
  fcv_parse_hex_color(hex_color, &r, &g, &b);

  int32_t radius_int32_t = (int32_t)radius;
  int32_t cx = (int32_t)center_x;
  int32_t cy = (int32_t)center_y;

  // Bresenham's circle algorithm for filled disk
  int32_t x = 0;
  int32_t y = radius_int32_t;
  int32_t d = 3 - 2 * radius_int32_t;

  // Initial filled lines
  fcv_fill_disk_lines(data, width, height, channels, cx, cy, x, y, r, g, b);

  while (y >= x) {
    x++;
    if (d > 0) {
      y--;
      d = d + 4 * (x - y) + 10;
    }
    else {
      d = d + 4 * x + 6;
    }
    fcv_fill_disk_lines(data, width, height, channels, cx, cy, x, y, r, g, b);
  }
}

/**
 * Add a colored border around an image.
 * Returns a new image with the border added.
 *
 * @param width Width of the input image.
 * @param height Height of the input image.
 * @param channels Number of channels in the image (1, 3, or 4).
 * @param hex_color Hex color code for the border (e.g., "FF0000" for red).
 * @param border_width Width of the border in pixels.
 * @param input_data Pointer to the input pixel data.
 * @param output_width Pointer to store the output image width.
 * @param output_height Pointer to store the output image height.
 * @return Pointer to the new image data with border, or NULL on failure.
 */
uint8_t *fcv_add_border(
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  const char *hex_color,
  uint32_t border_width,
  uint8_t *input_data,
  uint32_t *output_width,
  uint32_t *output_height
) {
  if (!input_data || !output_width || !output_height || !hex_color ||
      border_width == 0 || channels == 0) {
    return NULL;
  }

  if (width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow in output dimensions
  if (border_width > (UINT32_MAX - width) / 2 ||
      border_width > (UINT32_MAX - height) / 2) {
    return NULL;
  }

  // Parse border color
  uint8_t r, g, b;
  fcv_parse_hex_color(hex_color, &r, &g, &b);

  // Calculate output dimensions
  *output_width = width + 2 * border_width;
  *output_height = height + 2 * border_width;

  // Check for overflow in memory allocation: output_width * output_height *
  // channels
  if (*output_width > SIZE_MAX / *output_height) {
    return NULL;
  }
  size_t num_pixels = (size_t)(*output_width) * (*output_height);
  if (num_pixels > SIZE_MAX / channels) {
    return NULL;
  }
  size_t output_size = num_pixels * channels;

  uint8_t *output_data = malloc(output_size);
  if (!output_data) {
    return NULL;
  }

  // Fill the entire output image with border color
  for (uint32_t y = 0; y < *output_height; y++) {
    for (uint32_t x = 0; x < *output_width; x++) {
      uint32_t pixel_index = (y * *output_width + x) * channels;

      if (channels == 1) {
        // Grayscale: use luminance formula
        output_data[pixel_index] = (uint8_t)(0.299 * r + 0.587 * g + 0.114 * b);
      }
      else if (channels >= 3) {
        output_data[pixel_index] = r;     // R
        output_data[pixel_index + 1] = g; // G
        output_data[pixel_index + 2] = b; // B
        if (channels == 4) {
          output_data[pixel_index + 3] = 255; // A
        }
      }
    }
  }

  // Copy the original image into the center
  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      uint32_t src_index = (y * width + x) * channels;
      uint32_t dst_index =
        ((y + border_width) * *output_width + (x + border_width)) * channels;

      for (uint32_t c = 0; c < channels; c++) {
        output_data[dst_index + c] = input_data[src_index + c];
      }
    }
  }

  return output_data;
}
// File: src/exif.c
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "exif.h"
#endif

static uint16_t read_u16(FILE *file, int is_little_endian) {
  uint8_t buf[2];
  if (fread(buf, 1, 2, file) != 2) {
    return 0;
  }
  if (is_little_endian) {
    return (uint16_t)(buf[0] | (buf[1] << 8));
  }
  return (uint16_t)((buf[0] << 8) | buf[1]);
}

static uint32_t read_u32(FILE *file, int is_little_endian) {
  uint8_t buf[4];
  if (fread(buf, 1, 4, file) != 4) {
    return 0;
  }
  if (is_little_endian) {
    return (uint32_t)(buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24));
  }
  return (uint32_t)((buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]);
}

/**
 * Parse the orientation tag from a TIFF/EXIF block. `tiff_start` must point
 * to the first byte of the TIFF header (the "II" / "MM" byte-order marker).
 * Returns the orientation value (1-8) or 1 if not found.
 */
static int32_t read_orientation_from_tiff(FILE *file, long tiff_start) {
  fseek(file, tiff_start, SEEK_SET);

  uint8_t tiff_header[2];
  if (fread(tiff_header, 1, 2, file) != 2) {
    return 1;
  }

  int is_little_endian = (tiff_header[0] == 'I' && tiff_header[1] == 'I');
  int is_big_endian = (tiff_header[0] == 'M' && tiff_header[1] == 'M');

  if (!is_little_endian && !is_big_endian) {
    return 1;
  }

  uint16_t magic = read_u16(file, is_little_endian);
  if (magic != 42) {
    return 1;
  }

  uint32_t ifd_offset = read_u32(file, is_little_endian);

  fseek(file, tiff_start + ifd_offset, SEEK_SET);

  uint16_t num_entries = read_u16(file, is_little_endian);

  for (uint16_t i = 0; i < num_entries; i++) {
    uint16_t tag = read_u16(file, is_little_endian);
    (void)read_u16(file, is_little_endian); // Skip type
    (void)read_u32(file, is_little_endian); // Skip count

    if (tag == 0x0112) { // Orientation
      return (int32_t)read_u16(file, is_little_endian);
    }
    else {
      fseek(file, 4, SEEK_CUR); // Skip value/offset
    }
  }

  return 1;
}

/**
 * Read the EXIF orientation from a JPEG file. `file` must be positioned just
 * after the two-byte SOI marker (0xFFD8).
 */
static int32_t get_jpeg_orientation(FILE *file) {
  uint8_t marker[2];
  while (fread(marker, 1, 2, file) == 2) {
    if (marker[0] != 0xFF) {
      break;
    }

    if (marker[1] == 0xD9 || marker[1] == 0xDA) { // EOI or SOS
      break;
    }

    uint16_t size = read_u16(file, 0); // Markers are always big-endian

    if (marker[1] == 0xE1) { // APP1 - EXIF
      uint8_t exif_header[6];
      if (fread(exif_header, 1, 6, file) != 6) {
        break;
      }

      if (memcmp(exif_header, "Exif\0\0", 6) != 0) {
        fseek(file, size - 2 - 6, SEEK_CUR);
        continue;
      }

      return read_orientation_from_tiff(file, ftell(file));
    }
    else {
      fseek(file, size - 2, SEEK_CUR);
    }
  }

  return 1;
}

/**
 * Read the EXIF orientation from a PNG file. `file` must be positioned just
 * after the eight-byte PNG signature.
 *
 * EXIF data is stored in an `eXIf` chunk whose payload is a raw TIFF block
 * (no leading "Exif\0\0" prefix, unlike JPEG's APP1 segment).
 */
static int32_t get_png_orientation(FILE *file) {
  uint8_t type[4];
  while (1) {
    uint32_t length = read_u32(file, 0); // Chunk fields are big-endian
    if (fread(type, 1, 4, file) != 4) {
      break;
    }

    if (memcmp(type, "eXIf", 4) == 0) {
      return read_orientation_from_tiff(file, ftell(file));
    }

    if (memcmp(type, "IEND", 4) == 0) {
      break;
    }

    fseek(file, (long)length + 4, SEEK_CUR); // Skip chunk data + CRC
  }

  return 1;
}

int32_t fcv_get_exif_orientation(char const *filename) {
  FILE *file = fopen(filename, "rb");
  if (!file) {
    return 1;
  }

  uint8_t signature[8];
  size_t read_count = fread(signature, 1, 8, file);

  int32_t orientation = 1;

  static uint8_t const png_signature[8] =
    {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};

  if (read_count >= 2 && signature[0] == 0xFF && signature[1] == 0xD8) {
    fseek(file, 2, SEEK_SET);
    orientation = get_jpeg_orientation(file);
  }
  else if (read_count == 8 && memcmp(signature, png_signature, 8) == 0) {
    orientation = get_png_orientation(file);
  }

  fclose(file);
  return orientation;
}
// File: src/extract_document.c
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef FLATCV_AMALGAMATION
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif

uint8_t *fcv_extract_document(
  uint32_t width,
  uint32_t height,
  uint8_t const *const data,
  uint32_t output_width,
  uint32_t output_height
) {
  if (!data) {
    fprintf(stderr, "Error: NULL image data\n");
    return NULL;
  }

  // Step 1: Detect corners of the document
  Corners detected_corners = fcv_detect_corners(data, width, height);

  // Step 2: Define destination corners (rectangular output)
  Corners dst_corners = {
    .tl_x = 0.0,
    .tl_y = 0.0,
    .tr_x = (double)(output_width - 1),
    .tr_y = 0.0,
    .br_x = (double)(output_width - 1),
    .br_y = (double)(output_height - 1),
    .bl_x = 0.0,
    .bl_y = (double)(output_height - 1)
  };

  // Step 3: Calculate perspective transformation matrix
  // Note: We need to map FROM destination TO source for the inverse transform
  Matrix3x3 *transform_matrix =
    fcv_calculate_perspective_transform(&dst_corners, &detected_corners);

  if (!transform_matrix) {
    fprintf(stderr, "Error: Failed to calculate perspective transform\n");
    return NULL;
  }

  // Step 4: Apply the transformation
  uint8_t *result = fcv_apply_matrix_3x3(
    width,
    height,
    (uint8_t *)data,
    output_width,
    output_height,
    transform_matrix
  );

  // Free the transformation matrix if it's not the identity matrix
  if (transform_matrix &&
      !(transform_matrix->m00 == 1.0 && transform_matrix->m01 == 0.0 &&
        transform_matrix->m02 == 0.0 && transform_matrix->m10 == 0.0 &&
        transform_matrix->m11 == 1.0 && transform_matrix->m12 == 0.0 &&
        transform_matrix->m20 == 0.0 && transform_matrix->m21 == 0.0 &&
        transform_matrix->m22 == 1.0)) {
    free(transform_matrix);
  }

  return result;
}

uint8_t *fcv_extract_document_auto(
  uint32_t width,
  uint32_t height,
  uint8_t const *const data,
  uint32_t *output_width,
  uint32_t *output_height
) {
  if (!data || !output_width || !output_height) {
    fprintf(stderr, "Error: NULL parameters\n");
    return NULL;
  }

  // Step 1: Detect corners of the document
  Corners detected_corners = fcv_detect_corners(data, width, height);

  // Step 2: Calculate optimal output dimensions based on detected corners
  // Calculate distances between corners to determine aspect ratio
  double top_width = sqrt(
    pow(detected_corners.tr_x - detected_corners.tl_x, 2) +
    pow(detected_corners.tr_y - detected_corners.tl_y, 2)
  );
  double bottom_width = sqrt(
    pow(detected_corners.br_x - detected_corners.bl_x, 2) +
    pow(detected_corners.br_y - detected_corners.bl_y, 2)
  );
  double left_height = sqrt(
    pow(detected_corners.bl_x - detected_corners.tl_x, 2) +
    pow(detected_corners.bl_y - detected_corners.tl_y, 2)
  );
  double right_height = sqrt(
    pow(detected_corners.br_x - detected_corners.tr_x, 2) +
    pow(detected_corners.br_y - detected_corners.tr_y, 2)
  );

  // Use maximum dimensions to preserve detail
  double max_width = fmax(top_width, bottom_width);
  double max_height = fmax(left_height, right_height);

  // Round to reasonable dimensions
  *output_width = (uint32_t)(max_width + 0.5);
  *output_height = (uint32_t)(max_height + 0.5);

  // Ensure minimum reasonable size
  if (*output_width < 100) {
    *output_width = 100;
  }
  if (*output_height < 100) {
    *output_height = 100;
  }

  // Step 3: Define destination corners (rectangular output)
  Corners dst_corners = {
    .tl_x = 0.0,
    .tl_y = 0.0,
    .tr_x = (double)(*output_width - 1),
    .tr_y = 0.0,
    .br_x = (double)(*output_width - 1),
    .br_y = (double)(*output_height - 1),
    .bl_x = 0.0,
    .bl_y = (double)(*output_height - 1)
  };

  // Step 4: Calculate perspective transformation matrix
  // Note: We need to map FROM destination TO source for the inverse transform
  Matrix3x3 *transform_matrix =
    fcv_calculate_perspective_transform(&dst_corners, &detected_corners);

  if (!transform_matrix) {
    fprintf(stderr, "Error: Failed to calculate perspective transform\n");
    return NULL;
  }

  // Step 5: Apply the transformation
  uint8_t *result = fcv_apply_matrix_3x3(
    width,
    height,
    (uint8_t *)data,
    *output_width,
    *output_height,
    transform_matrix
  );

  // Free the transformation matrix if it's not the identity matrix
  if (transform_matrix &&
      !(transform_matrix->m00 == 1.0 && transform_matrix->m01 == 0.0 &&
        transform_matrix->m02 == 0.0 && transform_matrix->m10 == 0.0 &&
        transform_matrix->m11 == 1.0 && transform_matrix->m12 == 0.0 &&
        transform_matrix->m20 == 0.0 && transform_matrix->m21 == 0.0 &&
        transform_matrix->m22 == 1.0)) {
    free(transform_matrix);
  }

  return result;
}
// File: src/flip.c
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "flip.h"
#else
#include "flatcv.h"
#endif

/**
 * Flip an image horizontally (mirror along vertical axis).
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the pixel data.
 * @return Pointer to the flipped image data.
 */
uint8_t *
fcv_flip_x(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *flipped_data = malloc(img_length_byte);

  if (!flipped_data) { // Memory allocation failed
    return NULL;
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      size_t src_index = ((size_t)y * width + x) * 4;
      size_t dst_index = ((size_t)y * width + (width - 1 - x)) * 4;

      flipped_data[dst_index] = data[src_index];         // R
      flipped_data[dst_index + 1] = data[src_index + 1]; // G
      flipped_data[dst_index + 2] = data[src_index + 2]; // B
      flipped_data[dst_index + 3] = data[src_index + 3]; // A
    }
  }

  return flipped_data;
}

/**
 * Flip an image vertically (mirror along horizontal axis).
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the pixel data.
 * @return Pointer to the flipped image data.
 */
uint8_t *
fcv_flip_y(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *flipped_data = malloc(img_length_byte);

  if (!flipped_data) { // Memory allocation failed
    return NULL;
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      size_t src_index = ((size_t)y * width + x) * 4;
      size_t dst_index = ((size_t)(height - 1 - y) * width + x) * 4;

      flipped_data[dst_index] = data[src_index];         // R
      flipped_data[dst_index + 1] = data[src_index + 1]; // G
      flipped_data[dst_index + 2] = data[src_index + 2]; // B
      flipped_data[dst_index + 3] = data[src_index + 3]; // A
    }
  }

  return flipped_data;
}

/**
 * Transpose an image (flip along main diagonal).
 */
uint8_t *
fcv_transpose(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *transposed_data = malloc(img_length_byte);

  if (!transposed_data) {
    return NULL;
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      size_t src_index = ((size_t)y * width + x) * 4;
      size_t dst_index = ((size_t)x * height + y) * 4;

      transposed_data[dst_index] = data[src_index];
      transposed_data[dst_index + 1] = data[src_index + 1];
      transposed_data[dst_index + 2] = data[src_index + 2];
      transposed_data[dst_index + 3] = data[src_index + 3];
    }
  }

  return transposed_data;
}

/**
 * Transverse an image (flip along anti-diagonal).
 */
uint8_t *
fcv_transverse(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *transposed_data = malloc(img_length_byte);

  if (!transposed_data) {
    return NULL;
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      size_t src_index = ((size_t)y * width + x) * 4;
      size_t dst_index =
        ((size_t)(width - 1 - x) * height + (height - 1 - y)) * 4;

      transposed_data[dst_index] = data[src_index];
      transposed_data[dst_index + 1] = data[src_index + 1];
      transposed_data[dst_index + 2] = data[src_index + 2];
      transposed_data[dst_index + 3] = data[src_index + 3];
    }
  }

  return transposed_data;
}
// File: src/flood_fill.c
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#include "flood_fill.h"
#else
#include "flatcv.h"
#endif

/**
 * Flood fill segmentation
 * using a seeded breadth-first traversal with threshold-based inclusion
 *
 * Starting from each marker, expands across neighboring pixels whose
 * grayscale values stay within the given threshold of that marker's value.
 * Every reached pixel is set to `new_value`.
 * Each marker is filled independently (relative to its own seed value),
 * so the result is the union of all individual fills.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param grayscale_data Pointer to the input grayscale image data.
 * @param markers Seeds from which to start the flood fill.
 * @param num_markers Number of seeds.
 * @param threshold Maximum absolute grayscale difference from the seed value.
 * @param new_value Replacement value written to filled pixels.
 * @param connectivity 4 (edge neighbors) or 8 (edge and diagonal neighbors).
 * @return Pointer to the filled image data,
 *   or NULL on invalid input or allocation failure.
 */
uint8_t *fcv_flood_fill(
  uint32_t width,
  uint32_t height,
  uint8_t const *const grayscale_data,
  Point2D const *markers,
  uint32_t num_markers,
  uint8_t threshold,
  uint8_t new_value,
  uint8_t connectivity
) {
  if (!grayscale_data || !markers || num_markers == 0 || width == 0 ||
      height == 0 || width > INT32_MAX || height > INT32_MAX ||
      (connectivity != 4 && connectivity != 8)) {
    return NULL;
  }

  // Range-check as double before casting: out-of-range float-to-int
  // conversion is undefined behavior, and truncation would map e.g. -0.5 to 0.
  for (uint32_t m = 0; m < num_markers; m++) {
    double x = markers[m].x;
    double y = markers[m].y;
    if (!isfinite(x) || !isfinite(y) || x < 0 || y < 0 || x >= (double)width ||
        y >= (double)height) {
      return NULL;
    }
  }

  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;
  if (img_length_px > SIZE_MAX / sizeof(size_t)) {
    return NULL;
  }

  uint8_t *output_data = malloc(img_length_px);
  bool *visited = calloc(img_length_px, sizeof(bool));
  // Each pixel is enqueued at most once per seed,
  // so a linear (non-wrapping) queue of image size suffices.
  // After a seed's traversal, it also holds exactly the visited pixels.
  size_t *queue = malloc(img_length_px * sizeof(size_t));
  if (!output_data || !visited || !queue) {
    free(output_data);
    free(visited);
    free(queue);
    return NULL;
  }
  memcpy(output_data, grayscale_data, img_length_px);

  // First 4 entries are edge neighbors, last 4 are diagonals
  int32_t const dx[] = {-1, 1, 0, 0, -1, 1, -1, 1};
  int32_t const dy[] = {0, 0, -1, 1, -1, -1, 1, 1};

  for (uint32_t m = 0; m < num_markers; m++) {
    uint32_t seed_x = (uint32_t)markers[m].x;
    uint32_t seed_y = (uint32_t)markers[m].y;
    size_t seed_idx = (size_t)seed_y * width + seed_x;
    uint8_t target_value = grayscale_data[seed_idx];

    size_t head = 0;
    size_t tail = 0;
    queue[tail++] = seed_idx;
    visited[seed_idx] = true;

    while (head < tail) {
      size_t idx = queue[head++];
      output_data[idx] = new_value;

      int32_t x = (int32_t)(idx % width);
      int32_t y = (int32_t)(idx / width);

      for (uint8_t d = 0; d < connectivity; d++) {
        int32_t nx = x + dx[d];
        int32_t ny = y + dy[d];
        if (nx < 0 || nx >= (int32_t)width || ny < 0 || ny >= (int32_t)height) {
          continue;
        }

        size_t neighbor_idx = (size_t)ny * width + (size_t)nx;
        if (visited[neighbor_idx]) {
          continue;
        }

        int32_t diff = (int32_t)grayscale_data[neighbor_idx] - target_value;
        if (diff < 0) {
          diff = -diff;
        }
        if (diff > threshold) {
          continue;
        }

        visited[neighbor_idx] = true;
        queue[tail++] = neighbor_idx;
      }
    }

    // Reset only the pixels visited by this seed,
    // so the next seed can traverse them relative to its own value
    for (size_t i = 0; i < tail; i++) {
      visited[queue[i]] = false;
    }
  }

  free(queue);
  free(visited);
  return output_data;
}
// File: src/foerstner_corner.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif

/** Implementation of the Foerstner corner measure response image.
 * Expected input is a grayscale image.
 */
uint8_t *fcv_foerstner_corner(
  uint32_t width,
  uint32_t height,
  uint8_t const *const gray_data,
  double sigma
) {
  if (!gray_data || width == 0 || height == 0) {
    return NULL;
  }

  // Allocate memory for gradient images
  double *grad_x = calloc(width * height, sizeof(double));
  double *grad_y = calloc(width * height, sizeof(double));

  if (!grad_x || !grad_y) {
    free(grad_x);
    free(grad_y);
    return NULL;
  }

  // Compute image gradients using Sobel-like operators
  for (uint32_t y = 1; y < height - 1; y++) {
    for (uint32_t x = 1; x < width - 1; x++) {
      uint32_t idx = y * width + x;

      // Sobel X kernel: [-1 0 1; -2 0 2; -1 0 1]
      double gx = 0.0;
      gx += -1.0 * gray_data[(y - 1) * width + (x - 1)];
      gx += 1.0 * gray_data[(y - 1) * width + (x + 1)];
      gx += -2.0 * gray_data[y * width + (x - 1)];
      gx += 2.0 * gray_data[y * width + (x + 1)];
      gx += -1.0 * gray_data[(y + 1) * width + (x - 1)];
      gx += 1.0 * gray_data[(y + 1) * width + (x + 1)];

      // Sobel Y kernel: [-1 -2 -1; 0 0 0; 1 2 1]
      double gy = 0.0;
      gy += -1.0 * gray_data[(y - 1) * width + (x - 1)];
      gy += -2.0 * gray_data[(y - 1) * width + x];
      gy += -1.0 * gray_data[(y - 1) * width + (x + 1)];
      gy += 1.0 * gray_data[(y + 1) * width + (x - 1)];
      gy += 2.0 * gray_data[(y + 1) * width + x];
      gy += 1.0 * gray_data[(y + 1) * width + (x + 1)];

      grad_x[idx] = gx / 8.0; // Normalize
      grad_y[idx] = gy / 8.0;
    }
  }

  // Compute gradient products for structure tensor
  double *Axx = malloc(width * height * sizeof(double));
  double *Axy = malloc(width * height * sizeof(double));
  double *Ayy = malloc(width * height * sizeof(double));

  if (!Axx || !Axy || !Ayy) {
    free(grad_x);
    free(grad_y);
    free(Axx);
    free(Axy);
    free(Ayy);
    return NULL;
  }

  for (uint32_t i = 0; i < width * height; i++) {
    Axx[i] = grad_x[i] * grad_x[i];
    Axy[i] = grad_x[i] * grad_y[i];
    Ayy[i] = grad_y[i] * grad_y[i];
  }

  // Apply Gaussian smoothing to structure tensor elements
  // For simplicity, use a basic box filter approximation when sigma is small
  int32_t kernel_size = (int32_t)(3 * sigma) | 1; // Ensure odd size
  if (kernel_size < 3) {
    kernel_size = 3;
  }
  int32_t half_kernel = kernel_size / 2;

  double *Axx_smooth = calloc(width * height, sizeof(double));
  double *Axy_smooth = calloc(width * height, sizeof(double));
  double *Ayy_smooth = calloc(width * height, sizeof(double));

  if (!Axx_smooth || !Axy_smooth || !Ayy_smooth) {
    free(grad_x);
    free(grad_y);
    free(Axx);
    free(Axy);
    free(Ayy);
    free(Axx_smooth);
    free(Axy_smooth);
    free(Ayy_smooth);
    return NULL;
  }

  // Simple box filter smoothing
  for (uint32_t y = half_kernel; y < height - half_kernel; y++) {
    for (uint32_t x = half_kernel; x < width - half_kernel; x++) {
      uint32_t idx = y * width + x;
      double sum_xx = 0.0, sum_xy = 0.0, sum_yy = 0.0;
      int32_t count = 0;

      for (int32_t ky = -half_kernel; ky <= half_kernel; ky++) {
        for (int32_t kx = -half_kernel; kx <= half_kernel; kx++) {
          int32_t sample_idx = (y + ky) * width + (x + kx);
          sum_xx += Axx[sample_idx];
          sum_xy += Axy[sample_idx];
          sum_yy += Ayy[sample_idx];
          count++;
        }
      }

      Axx_smooth[idx] = sum_xx / count;
      Axy_smooth[idx] = sum_xy / count;
      Ayy_smooth[idx] = sum_yy / count;
    }
  }

  // Compute Foerstner measures w and q
  uint8_t *result = malloc(width * height * 2); // 2 channels: w, q
  if (!result) {
    free(grad_x);
    free(grad_y);
    free(Axx);
    free(Axy);
    free(Ayy);
    free(Axx_smooth);
    free(Axy_smooth);
    free(Ayy_smooth);
    return NULL;
  }

  double max_w = 0.0, max_q = 0.0;
  double *w_values = malloc(width * height * sizeof(double));
  double *q_values = malloc(width * height * sizeof(double));
  if (!w_values || !q_values) {
    free(result);
    free(w_values);
    free(q_values);
    free(grad_x);
    free(grad_y);
    free(Axx);
    free(Axy);
    free(Ayy);
    free(Axx_smooth);
    free(Axy_smooth);
    free(Ayy_smooth);
    return NULL;
  }

  // First pass: compute w and q values and find maximum for normalization
  for (uint32_t i = 0; i < width * height; i++) {
    double det_A =
      Axx_smooth[i] * Ayy_smooth[i] - Axy_smooth[i] * Axy_smooth[i];
    double trace_A = Axx_smooth[i] + Ayy_smooth[i];

    double w = 0.0, q = 0.0;

    if (fabs(trace_A) > 1e-10) { // Avoid division by zero
      w = det_A / trace_A;
      q = 4.0 * det_A / (trace_A * trace_A);
    }

    w_values[i] = fmax(0.0, w); // Ensure non-negative
    q_values[i] = fmax(0.0, q);

    if (w_values[i] > max_w) {
      max_w = w_values[i];
    }
    if (q_values[i] > max_q) {
      max_q = q_values[i];
    }
  }

  // Second pass: normalize and convert to bytes
  for (uint32_t i = 0; i < width * height; i++) {
    uint8_t w_byte = 0, q_byte = 0;

    if (max_w > 0.0) {
      w_byte = (uint8_t)(255.0 * w_values[i] / max_w);
    }
    if (max_q > 0.0) {
      q_byte = (uint8_t)(255.0 * q_values[i] / max_q);
    }

    result[i * 2] = w_byte;     // w measure
    result[i * 2 + 1] = q_byte; // q measure
  }

  free(w_values);
  free(q_values);

  // Cleanup
  free(grad_x);
  free(grad_y);
  free(Axx);
  free(Axy);
  free(Ayy);
  free(Axx_smooth);
  free(Axy_smooth);
  free(Ayy_smooth);

  return result;
}
// File: src/histogram.c
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "histogram.h"
#else
#include "flatcv.h"
#endif

/**
 * Generate a histogram visualization image from input image data.
 * For grayscale images, creates a single histogram.
 * For RGB(A) images, creates overlapping histograms for each channel.
 *
 * @param width Width of the input image.
 * @param height Height of the input image.
 * @param channels Number of channels in the input image (1, 3, or 4).
 * @param data Pointer to the input pixel data.
 * @param out_width Pointer to store the output histogram width.
 * @param out_height Pointer to store the output histogram height.
 * @return Pointer to the histogram image data (RGBA format).
 */
uint8_t *fcv_generate_histogram(
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  uint8_t const *const data,
  uint32_t *out_width,
  uint32_t *out_height
) {
  if (!data || !out_width || !out_height) {
    return NULL;
  }

  if (width == 0 || height == 0 || channels == 0) {
    return NULL;
  }

  // Check for overflow in img_size calculation
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_size = (size_t)width * height;

  // Validate that we can access all pixels
  if (img_size > SIZE_MAX / channels) {
    return NULL;
  }

  // Output histogram dimensions
  const uint32_t hist_width = 256;  // One pixel per brightness value
  const uint32_t hist_height = 200; // Fixed height for visualization

  *out_width = hist_width;
  *out_height = hist_height;

  // Output size is fixed: 256 * 200 * 4 = 204800 bytes (no overflow possible)
  size_t output_size = (size_t)hist_width * hist_height * 4;

  // Allocate output image (initialized to black background)
  uint8_t *output = calloc(output_size, sizeof(uint8_t));
  if (!output) {
    return NULL;
  }

  // Initialize with black background and full alpha
  for (uint32_t i = 0; i < (*out_width) * (*out_height); i++) {
    output[i * 4 + 3] = 255; // Alpha channel
  }

  // Count histograms for each channel
  uint32_t hist_r[256] = {0};
  uint32_t hist_g[256] = {0};
  uint32_t hist_b[256] = {0};
  uint32_t hist_gray[256] = {0};

  bool is_grayscale = (channels == 1);
  if (channels == 4) {
    // Check if it's actually grayscale (all RGB channels equal)
    is_grayscale = true;
    for (size_t i = 0; i < img_size && is_grayscale; i++) {
      size_t idx = i * 4;
      if (data[idx] != data[idx + 1] || data[idx] != data[idx + 2]) {
        is_grayscale = false;
      }
    }
  }

  if (is_grayscale) {
    // Single channel or grayscale image
    for (size_t i = 0; i < img_size; i++) {
      uint8_t value;
      if (channels == 1) {
        value = data[i];
      }
      else {
        // Use the red channel for grayscale RGBA
        value = data[i * 4];
      }
      hist_gray[value]++;
    }
  }
  else {
    // RGB or RGBA image - count each channel
    for (size_t i = 0; i < img_size; i++) {
      size_t idx = i * channels;
      hist_r[data[idx]]++;
      if (channels >= 3) {
        hist_g[data[idx + 1]]++;
        hist_b[data[idx + 2]]++;
      }
    }
  }

  // Find maximum count for scaling
  uint32_t max_count = 0;
  if (is_grayscale) {
    for (int i = 0; i < 256; i++) {
      if (hist_gray[i] > max_count) {
        max_count = hist_gray[i];
      }
    }
  }
  else {
    for (int i = 0; i < 256; i++) {
      if (hist_r[i] > max_count) {
        max_count = hist_r[i];
      }
      if (channels >= 3) {
        if (hist_g[i] > max_count) {
          max_count = hist_g[i];
        }
        if (hist_b[i] > max_count) {
          max_count = hist_b[i];
        }
      }
    }
  }

  if (max_count == 0) {
    return output; // Return black image if no data
  }

  // Draw histogram bars
  for (int x = 0; x < 256; x++) {
    if (is_grayscale) {
      // Draw white histogram for grayscale
      uint32_t bar_height = (hist_gray[x] * hist_height) / max_count;
      for (uint32_t y = 0; y < bar_height; y++) {
        uint32_t output_x = x;
        uint32_t output_y = hist_height - 1 - y;
        uint32_t output_idx = (output_y * (*out_width) + output_x) * 4;

        output[output_idx] = 255;     // R
        output[output_idx + 1] = 255; // G
        output[output_idx + 2] = 255; // B
        output[output_idx + 3] = 255; // A
      }
    }
    else {
      // Draw overlapping colored histograms for RGB
      uint32_t bar_height_r = (hist_r[x] * hist_height) / max_count;
      uint32_t bar_height_g =
        (channels >= 3) ? (hist_g[x] * hist_height) / max_count : 0;
      uint32_t bar_height_b =
        (channels >= 3) ? (hist_b[x] * hist_height) / max_count : 0;

      uint32_t max_bar_height = bar_height_r;
      if (bar_height_g > max_bar_height) {
        max_bar_height = bar_height_g;
      }
      if (bar_height_b > max_bar_height) {
        max_bar_height = bar_height_b;
      }

      for (uint32_t y = 0; y < max_bar_height; y++) {
        uint32_t output_x = x;
        uint32_t output_y = hist_height - 1 - y;
        uint32_t output_idx = (output_y * (*out_width) + output_x) * 4;

        // Initialize to black
        uint8_t r = 0, g = 0, b = 0;

        // Add red channel contribution
        if (y < bar_height_r) {
          r = 255;
        }

        // Add green channel contribution
        if (channels >= 3 && y < bar_height_g) {
          g = 255;
        }

        // Add blue channel contribution
        if (channels >= 3 && y < bar_height_b) {
          b = 255;
        }

        output[output_idx] = r;       // R
        output[output_idx + 1] = g;   // G
        output[output_idx + 2] = b;   // B
        output[output_idx + 3] = 255; // A
      }
    }
  }

  return output;
}
// File: src/parse_hex_color.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "parse_hex_color.h"
#else
#include "flatcv.h"
#endif

// Helper function to parse hex color code to RGB values
void fcv_parse_hex_color(
  const char *hex_color,
  uint8_t *r,
  uint8_t *g,
  uint8_t *b
) {
  const char *hex = hex_color;

  // Skip '#' if present
  if (hex[0] == '#') {
    hex++;
  }

  // Validate hex string length (should be 6 characters)
  if (strlen(hex) != 6) {
    // Default to white for invalid hex codes
    *r = 255;
    *g = 255;
    *b = 255;
    return;
  }

  // Validate that all characters are valid hex digits
  for (int32_t i = 0; i < 6; i++) {
    if (!((hex[i] >= '0' && hex[i] <= '9') ||
          (hex[i] >= 'A' && hex[i] <= 'F') ||
          (hex[i] >= 'a' && hex[i] <= 'f'))) {
      // Default to white for invalid hex codes
      *r = 255;
      *g = 255;
      *b = 255;
      return;
    }
  }

  // Parse hex values
  uint32_t rgb_value;
  sscanf(hex, "%x", &rgb_value);

  *r = (rgb_value >> 16) & 0xFF;
  *g = (rgb_value >> 8) & 0xFF;
  *b = rgb_value & 0xFF;
}
// File: src/perspectivetransform.c
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "perspectivetransform.h"
#else
#include "flatcv.h"
#endif

// #define DEBUG_LOGGING

#ifdef DEBUG_LOGGING
#define log(msg) printf("DEBUG: %s\n", msg)
#else
#define log(msg) // No operation
#endif

/**
 * Helper function to solve 8x8 linear system using Gaussian elimination
 * Returns 1 on success, 0 on failure
 */
int32_t solve_linear_system(double A[8][8], double b[8], double x[8]) {
  const int32_t n = 8;
  const double epsilon = 1e-10;
  int32_t i, j, k;

  // Create augmented matrix [A|b] with extra safety margin
  double aug[8][10]; // One extra column for safety

  // Initialize augmented matrix
  for (i = 0; i < n; i++) {
    for (j = 0; j < n; j++) {
      aug[i][j] = A[i][j];
    }
    aug[i][n] = b[i];
  }

  // Gaussian elimination with partial pivoting
  for (i = 0; i < n; i++) {
    // Find pivot
    int32_t max_row = i;
    double max_val = fabs(aug[i][i]);

    for (k = i + 1; k < n; k++) {
      if (fabs(aug[k][i]) > max_val) {
        max_val = fabs(aug[k][i]);
        max_row = k;
      }
    }

    // Check for singularity
    if (max_val < epsilon) {
      log("Warning: Matrix is nearly singular\n");
      return 0;
    }

    // Swap maximum row with current row
    if (max_row != i) {
      for (j = 0; j <= n; j++) {
        double temp = aug[i][j];
        aug[i][j] = aug[max_row][j];
        aug[max_row][j] = temp;
      }
    }

    // Eliminate column i
    for (j = i + 1; j < n; j++) {
      double factor = aug[j][i] / aug[i][i];
      for (k = i; k <= n; k++) {
        aug[j][k] -= factor * aug[i][k];
      }
    }
  }

  // Back substitution
  for (i = n - 1; i >= 0; i--) {
    if (fabs(aug[i][i]) < epsilon) {
      log("Warning: Zero pivot encountered\n");
      return 0;
    }

    x[i] = aug[i][n];
    for (j = i + 1; j < n; j++) {
      x[i] -= aug[i][j] * x[j];
    }
    x[i] /= aug[i][i];

    // Check for invalid results
    if (isnan(x[i]) || isinf(x[i])) {
      log("Warning: Invalid result detected\n");
      return 0;
    }
  }

  return 1;
}

/**
 * Calculate the perspective transformation matrix
 * from the source and destination corner coordinates.
 */
Matrix3x3 *fcv_calculate_perspective_transform(
  Corners *src_corners,
  Corners *dst_corners
) {
  // Initialize matrices with zeros
  double A[8][8] = {{0}};
  double b[8] = {0};
  double x[8] = {0};

  // Identity matrix as fallback
  static Matrix3x3 identity = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0};

  if (!src_corners || !dst_corners) {
    log("Error: NULL pointer passed to calculate_perspective_transform\n");
    return &identity;
  }

#ifdef DEBUG_LOGGING
  fprintf(stderr, "[C] Calculating perspective transform:\n");
  fprintf(
    stderr,
    "src_corners:\ntl(%f, %f)\ntr(%f, %f)\nbr(%f, %f)\nbl(%f, %f)\n\n",
    src_corners->tl_x,
    src_corners->tl_y,
    src_corners->tr_x,
    src_corners->tr_y,
    src_corners->br_x,
    src_corners->br_y,
    src_corners->bl_x,
    src_corners->bl_y
  );
  fprintf(
    stderr,
    "dst_corners:\ntl(%f, %f)\ntr(%f, %f)\nbr(%f, %f)\nbl(%f, %f)\n\n",
    dst_corners->tl_x,
    dst_corners->tl_y,
    dst_corners->tr_x,
    dst_corners->tr_y,
    dst_corners->br_x,
    dst_corners->br_y,
    dst_corners->bl_x,
    dst_corners->bl_y
  );
#endif

  // Validate input coordinates
  if (isnan(src_corners->tl_x) || isnan(src_corners->tl_y) ||
      isnan(src_corners->tr_x) || isnan(src_corners->tr_y) ||
      isnan(src_corners->br_x) || isnan(src_corners->br_y) ||
      isnan(src_corners->bl_x) || isnan(src_corners->bl_y) ||
      isnan(dst_corners->tl_x) || isnan(dst_corners->tl_y) ||
      isnan(dst_corners->tr_x) || isnan(dst_corners->tr_y) ||
      isnan(dst_corners->br_x) || isnan(dst_corners->br_y) ||
      isnan(dst_corners->bl_x) || isnan(dst_corners->bl_y)) {
    log("Error: Invalid coordinates (NaN) detected\n");
    return &identity;
  }

  // Set up the system of equations
  for (int32_t i = 0; i < 4; i++) {
    double srcX = 0.0, srcY = 0.0, dstX = 0.0, dstY = 0.0;

    // Safely extract coordinates
    switch (i) {
    case 0: // Top-left
      srcX = src_corners->tl_x;
      srcY = src_corners->tl_y;
      dstX = dst_corners->tl_x;
      dstY = dst_corners->tl_y;
      break;
    case 1: // Top-right
      srcX = src_corners->tr_x;
      srcY = src_corners->tr_y;
      dstX = dst_corners->tr_x;
      dstY = dst_corners->tr_y;
      break;
    case 2: // Bottom-right
      srcX = src_corners->br_x;
      srcY = src_corners->br_y;
      dstX = dst_corners->br_x;
      dstY = dst_corners->br_y;
      break;
    case 3: // Bottom-left
      srcX = src_corners->bl_x;
      srcY = src_corners->bl_y;
      dstX = dst_corners->bl_x;
      dstY = dst_corners->bl_y;
      break;
    }

    // Validate extracted coordinates
    if (isinf(srcX) || isinf(srcY) || isinf(dstX) || isinf(dstY)) {
      log("Error: Invalid coordinates (Inf) detected\n");
      return &identity;
    }

    // First four equations for x coordinates
    A[i][0] = srcX;
    A[i][1] = srcY;
    A[i][2] = 1.0;
    A[i][6] = -srcX * dstX;
    A[i][7] = -srcY * dstX;
    b[i] = dstX;

    // Last four equations for y coordinates
    A[i + 4][3] = srcX;
    A[i + 4][4] = srcY;
    A[i + 4][5] = 1.0;
    A[i + 4][6] = -srcX * dstY;
    A[i + 4][7] = -srcY * dstY;
    b[i + 4] = dstY;
  }

  log("Solve the system of equations …\n");
  if (!solve_linear_system(A, b, x)) {
    log("Failed to solve system, returning identity matrix\n");
    return &identity;
  }

  // Validate solution
  for (int32_t i = 0; i < 8; i++) {
    if (isnan(x[i]) || isinf(x[i]) || fabs(x[i]) > 1e6) {
      log("Error: Invalid solution values detected\n");
      return &identity;
    }
  }

  Matrix3x3 *result = malloc(sizeof(Matrix3x3));
  *result = (Matrix3x3){x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], 1.0};

#ifdef DEBUG_LOGGING
  fprintf(stderr, "Result matrix:\n");
  fprintf(stderr, "%f, %f, %f\n", result->m00, result->m01, result->m02);
  fprintf(stderr, "%f, %f, %f\n", result->m10, result->m11, result->m12);
  fprintf(stderr, "%f, %f, %f\n", result->m20, result->m21, result->m22);
#endif

  // Final validation of the result matrix
  if (isnan(result->m00) || isnan(result->m01) || isnan(result->m02) ||
      isnan(result->m10) || isnan(result->m11) || isnan(result->m12) ||
      isnan(result->m20) || isnan(result->m21) || isnan(result->m22) ||
      isinf(result->m00) || isinf(result->m01) || isinf(result->m02) ||
      isinf(result->m10) || isinf(result->m11) || isinf(result->m12) ||
      isinf(result->m20) || isinf(result->m21) || isinf(result->m22)) {
    log("Error: Invalid values in result matrix\n");
    return &identity;
  }

  return result;
}

/**
 * Apply the transformation matrix to the input image
 * and store the result in the output image.
 * Use bilinear interpolation to calculate final pixel values.
 */
uint8_t *fcv_apply_matrix_3x3(
  int32_t in_width,
  int32_t in_height,
  uint8_t *in_data,
  int32_t out_width,
  int32_t out_height,
  Matrix3x3 *tmat
) {
  if (!in_data || !tmat) {
    return NULL;
  }

  if (in_width <= 0 || in_height <= 0 || out_width <= 0 || out_height <= 0) {
    return NULL;
  }

  // Check for overflow: out_width * out_height * 4 (dimensions already
  // validated > 0)
  if ((size_t)out_width > SIZE_MAX / (size_t)out_height) {
    return NULL;
  }
  size_t out_pixels = (size_t)out_width * (size_t)out_height;
  if (out_pixels > SIZE_MAX / 4) {
    return NULL;
  }

  // Patch flip matrix if needed
  if (fabs(tmat->m00 + 1.0) < 1e-9 && fabs(tmat->m11 + 1.0) < 1e-9 &&
      tmat->m02 == 0.0 && tmat->m12 == 0.0) {
    tmat->m02 = in_width - 1;
    tmat->m12 = in_height - 1;
  }

  uint8_t *out_data = calloc(out_pixels * 4, sizeof(uint8_t));

  if (!out_data) { // Memory allocation failed
    return NULL;
  }

  // Iterate through every pixel in the output image
  for (int32_t out_y = 0; out_y < out_height; ++out_y) {
    for (int32_t out_x = 0; out_x < out_width; ++out_x) {
      // Apply the inverse transformation to find the corresponding source pixel
      double w = tmat->m20 * out_x + tmat->m21 * out_y + tmat->m22;
      if (fabs(w) < 1e-10) {
        continue; // Skip if w is too close to zero
      }

      double srcX = (tmat->m00 * out_x + tmat->m01 * out_y + tmat->m02) / w;
      double srcY = (tmat->m10 * out_x + tmat->m11 * out_y + tmat->m12) / w;

      // Convert source coordinates to integers
      int32_t x0 = (int32_t)floor(srcX);
      int32_t y0 = (int32_t)floor(srcY);
      int32_t x1 = x0 + 1;
      int32_t y1 = y0 + 1;

      // Verify that the anchor pixel is inside the source image
      if (x0 >= 0 && x0 < in_width && y0 >= 0 && y0 < in_height) {

        // Clamp the neighbor coordinates so that a (degenerated)
        // bilinear interpolation can be applied at the image borders.
        int32_t x1c = (x1 < in_width) ? x1 : x0;
        int32_t y1c = (y1 < in_height) ? y1 : y0;

        double dx = srcX - x0;
        double dy = srcY - y0;

        // If a neighbour got clamped we force the corresponding weight to 0
        if (x1c == x0) {
          dx = 0.0;
        }
        if (y1c == y0) {
          dy = 0.0;
        }

        uint8_t *p00 = &in_data[(y0 * in_width + x0) * 4];
        uint8_t *p01 = &in_data[(y0 * in_width + x1c) * 4];
        uint8_t *p10 = &in_data[(y1c * in_width + x0) * 4];
        uint8_t *p11 = &in_data[(y1c * in_width + x1c) * 4];

        for (int32_t c = 0; c < 4; ++c) {
          out_data[(out_y * out_width + out_x) * 4 + c] =
            (uint8_t)(p00[c] * (1 - dx) * (1 - dy) + p01[c] * dx * (1 - dy) +
                      p10[c] * (1 - dx) * dy + p11[c] * dx * dy);
        }
      }
    }
  }

  return out_data;
}
// File: src/qr_code.c
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "qr_code.h"
#else
#include "flatcv.h"
#endif

#define MAX_QR_CODES 10
#define MAX_CANDIDATES 200
#define MAX_TRIPLES 10
/* Max alignment patterns per QR. Version 40 has 7×7 = 49 grid positions,
   of which 3 (top-left, top-right, bottom-left) overlap the finder patterns
   and are skipped — leaving at most 46. */
#define QR_MAX_ALIGNMENTS 46

/* ---- Types ---- */

typedef struct {
  float x, y, module_size;
} FinderPattern;

/* ---- Adaptive binarization ----
   Hybrid thresholding: combines a global Otsu threshold with a local-variance
   check via a mean/square integral image. In high-contrast windows we
   threshold against the local mean (handles uneven illumination on e-ink
   displays and perspective-lit photos); in low-contrast windows we fall back
   to the global Otsu threshold (preserves large uniform dark/light regions
   that a pure local mean would invert).
   Output buffer: 0 for dark, 255 for light. */
static int compute_otsu_threshold(uint8_t const *gray, int w, int h) {
  size_t n = (size_t)w * (size_t)h;
  uint32_t hist[256] = {0};
  for (size_t i = 0; i < n; i++) {
    hist[gray[i]]++;
  }
  double total = (double)n;
  double sum_all = 0.0;
  for (int i = 0; i < 256; i++) {
    sum_all += (double)i * hist[i];
  }
  double w_bg = 0.0, sum_bg = 0.0;
  double max_var = 0.0;
  int best_t = 128;
  for (int t = 0; t < 256; t++) {
    w_bg += hist[t];
    if (w_bg == 0.0 || w_bg == total) {
      continue;
    }
    sum_bg += (double)t * hist[t];
    double w_fg = total - w_bg;
    double mean_bg = sum_bg / w_bg;
    double mean_fg = (sum_all - sum_bg) / w_fg;
    double diff = mean_bg - mean_fg;
    double var_between = w_bg * w_fg * diff * diff;
    if (var_between > max_var) {
      max_var = var_between;
      best_t = t;
    }
  }
  return best_t;
}

static uint8_t *binarize_global_otsu(uint8_t const *gray, int w, int h) {
  if (w <= 0 || h <= 0) {
    return NULL;
  }
  size_t n = (size_t)w * (size_t)h;
  uint8_t *bin = malloc(n);
  if (!bin) {
    return NULL;
  }
  int t = compute_otsu_threshold(gray, w, h);
  for (size_t i = 0; i < n; i++) {
    bin[i] = (gray[i] < t) ? 0 : 255;
  }
  return bin;
}

static uint8_t *binarize_adaptive(uint8_t const *gray, int w, int h) {
  if (w <= 0 || h <= 0) {
    return NULL;
  }
  size_t n = (size_t)w * (size_t)h;
  uint8_t *bin = malloc(n);
  if (!bin) {
    return NULL;
  }
  size_t iw = (size_t)w + 1;
  uint64_t *ii_sum = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
  uint64_t *ii_sq = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
  if (!ii_sum || !ii_sq) {
    free(bin);
    free(ii_sum);
    free(ii_sq);
    return NULL;
  }
  for (int y = 0; y < h; y++) {
    uint64_t row_sum = 0;
    uint64_t row_sq = 0;
    for (int x = 0; x < w; x++) {
      uint8_t v = gray[y * w + x];
      row_sum += v;
      row_sq += (uint64_t)v * v;
      size_t idx = (size_t)(y + 1) * iw + (size_t)(x + 1);
      ii_sum[idx] = ii_sum[idx - iw] + row_sum;
      ii_sq[idx] = ii_sq[idx - iw] + row_sq;
    }
  }
  int global_t = compute_otsu_threshold(gray, w, h);
  int block = (w < h ? w : h) / 8;
  if (block < 15) {
    block = 15;
  }
  if (block > 101) {
    block = 101;
  }
  if ((block & 1) == 0) {
    block++;
  }
  int half = block / 2;
  /* Below min_std the window is treated as uniform and the global threshold
     decides the pixel class. This keeps large uniform dark regions (e.g.
     finder-pattern centers, dark backgrounds) from being inverted by a local
     mean that sits right on the pixel value. */
  const double min_std = 24.0;
  for (int y = 0; y < h; y++) {
    int y0 = y - half;
    if (y0 < 0) {
      y0 = 0;
    }
    int y1 = y + half;
    if (y1 >= h) {
      y1 = h - 1;
    }
    for (int x = 0; x < w; x++) {
      int x0 = x - half;
      if (x0 < 0) {
        x0 = 0;
      }
      int x1 = x + half;
      if (x1 >= w) {
        x1 = w - 1;
      }
      double count = (double)((y1 - y0 + 1) * (x1 - x0 + 1));
      size_t a = (size_t)(y1 + 1) * iw + (size_t)(x1 + 1);
      size_t b = (size_t)y0 * iw + (size_t)(x1 + 1);
      size_t c = (size_t)(y1 + 1) * iw + (size_t)x0;
      size_t d = (size_t)y0 * iw + (size_t)x0;
      double sum = (double)(ii_sum[a] - ii_sum[b] - ii_sum[c] + ii_sum[d]);
      double sqsum = (double)(ii_sq[a] - ii_sq[b] - ii_sq[c] + ii_sq[d]);
      double mean = sum / count;
      double var = sqsum / count - mean * mean;
      if (var < 0.0) {
        var = 0.0;
      }
      double std = sqrt(var);
      uint8_t gv = gray[y * w + x];
      int dark;
      if (std < min_std) {
        dark = (gv < global_t);
      }
      else {
        dark = ((double)gv < mean);
      }
      bin[y * w + x] = dark ? 0 : 255;
    }
  }
  free(ii_sum);
  free(ii_sq);
  return bin;
}

/* ---- Sauvola binarization ----
   T = mean * (1 + k * (std / R - 1)), k=0.34, R=128. Scales the threshold
   by local std, which catches faded/low-contrast modules that the fixed
   min_std fallback in binarize_adaptive misclassifies. Block deliberately
   smaller than binarize_adaptive's so it stays responsive when modules are
   only 2-3 px wide. */
static uint8_t *binarize_sauvola(uint8_t const *gray, int w, int h) {
  if (w <= 0 || h <= 0) {
    return NULL;
  }
  size_t n = (size_t)w * (size_t)h;
  uint8_t *bin = malloc(n);
  if (!bin) {
    return NULL;
  }
  size_t iw = (size_t)w + 1;
  uint64_t *ii_sum = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
  uint64_t *ii_sq = calloc(iw * (size_t)(h + 1), sizeof(uint64_t));
  if (!ii_sum || !ii_sq) {
    free(bin);
    free(ii_sum);
    free(ii_sq);
    return NULL;
  }
  for (int y = 0; y < h; y++) {
    uint64_t row_sum = 0;
    uint64_t row_sq = 0;
    for (int x = 0; x < w; x++) {
      uint8_t v = gray[y * w + x];
      row_sum += v;
      row_sq += (uint64_t)v * v;
      size_t idx = (size_t)(y + 1) * iw + (size_t)(x + 1);
      ii_sum[idx] = ii_sum[idx - iw] + row_sum;
      ii_sq[idx] = ii_sq[idx - iw] + row_sq;
    }
  }
  int block = (w < h ? w : h) / 12;
  if (block < 11) {
    block = 11;
  }
  if (block > 81) {
    block = 81;
  }
  if ((block & 1) == 0) {
    block++;
  }
  int half = block / 2;
  const double k = 0.34;
  const double R = 128.0;
  for (int y = 0; y < h; y++) {
    int y0 = y - half;
    if (y0 < 0) {
      y0 = 0;
    }
    int y1 = y + half;
    if (y1 >= h) {
      y1 = h - 1;
    }
    for (int x = 0; x < w; x++) {
      int x0 = x - half;
      if (x0 < 0) {
        x0 = 0;
      }
      int x1 = x + half;
      if (x1 >= w) {
        x1 = w - 1;
      }
      double count = (double)((y1 - y0 + 1) * (x1 - x0 + 1));
      size_t a = (size_t)(y1 + 1) * iw + (size_t)(x1 + 1);
      size_t b = (size_t)y0 * iw + (size_t)(x1 + 1);
      size_t c = (size_t)(y1 + 1) * iw + (size_t)x0;
      size_t d = (size_t)y0 * iw + (size_t)x0;
      double sum = (double)(ii_sum[a] - ii_sum[b] - ii_sum[c] + ii_sum[d]);
      double sqsum = (double)(ii_sq[a] - ii_sq[b] - ii_sq[c] + ii_sq[d]);
      double mean = sum / count;
      double var = sqsum / count - mean * mean;
      if (var < 0.0) {
        var = 0.0;
      }
      double std = sqrt(var);
      double thresh = mean * (1.0 + k * (std / R - 1.0));
      uint8_t gv = gray[y * w + x];
      bin[y * w + x] = ((double)gv < thresh) ? 0 : 255;
    }
  }
  free(ii_sum);
  free(ii_sq);
  return bin;
}

/* ---- Hybrid binarization (zxing-cpp port) ----
   Port of zxing-cpp's HybridBinarizer (core/src/HybridBinarizer.cpp, NEW
   algorithm). Works in 8x8 blocks and computes a per-block threshold of
   (min+max)/2 only when the dynamic range exceeds 24; low-contrast blocks
   are marked "unknown" and inherit their threshold from a 5x5 smoothing
   over the *known* neighbours, followed by raster flood-fill of any
   surviving zeros. This is substantially more robust than binarize_adaptive
   on low-contrast pastel palettes: flatcv's adaptive falls back to the
   global Otsu threshold whenever local std<24, which misclassifies entire
   interior regions; zxing's scheme instead propagates an edge-of-QR
   threshold inward.
   Output: 0 = dark, 255 = light. Returns NULL if image smaller than the
   5x5 window (40 px) — caller should fall back to global Otsu. */
#define HB_BLOCK_SIZE 8
#define HB_WINDOW_BLOCKS 5
#define HB_WINDOW_SIZE (HB_BLOCK_SIZE * HB_WINDOW_BLOCKS)
#define HB_MIN_DYNAMIC_RANGE 24

static uint8_t *binarize_hybrid(uint8_t const *gray, int w, int h) {
  if (w < HB_WINDOW_SIZE || h < HB_WINDOW_SIZE) {
    return NULL;
  }
  int sub_w = (w + HB_BLOCK_SIZE - 1) / HB_BLOCK_SIZE;
  int sub_h = (h + HB_BLOCK_SIZE - 1) / HB_BLOCK_SIZE;
  size_t sub_n = (size_t)sub_w * (size_t)sub_h;
  uint8_t *raw = malloc(sub_n);
  uint8_t *smoothed = malloc(sub_n);
  uint8_t *bin = malloc((size_t)w * (size_t)h);
  if (!raw || !smoothed || !bin) {
    free(raw);
    free(smoothed);
    free(bin);
    return NULL;
  }

  /* Per-block threshold: (max+min)/2 if range exceeds min_dynamic_range,
     else 0 (unknown — filled in by smoothing). Last row/col of blocks is
     clamped so the 8x8 window stays fully inside the image. */
  for (int by = 0; by < sub_h; by++) {
    int y0 = by * HB_BLOCK_SIZE;
    if (y0 > h - HB_BLOCK_SIZE) {
      y0 = h - HB_BLOCK_SIZE;
    }
    for (int bx = 0; bx < sub_w; bx++) {
      int x0 = bx * HB_BLOCK_SIZE;
      if (x0 > w - HB_BLOCK_SIZE) {
        x0 = w - HB_BLOCK_SIZE;
      }
      uint8_t mn = 255, mx = 0;
      for (int yy = 0; yy < HB_BLOCK_SIZE; yy++) {
        uint8_t const *row = gray + (size_t)(y0 + yy) * (size_t)w + x0;
        for (int xx = 0; xx < HB_BLOCK_SIZE; xx++) {
          uint8_t v = row[xx];
          if (v < mn) {
            mn = v;
          }
          if (v > mx) {
            mx = v;
          }
        }
      }
      raw[(size_t)by * sub_w + bx] =
        (mx - mn > HB_MIN_DYNAMIC_RANGE) ? (uint8_t)(((int)mx + mn) / 2) : 0;
    }
  }

  /* 5x5 gaussian-like smoothing over non-zero thresholds (center counted
     twice). Low-contrast blocks with no contrasty neighbour remain 0. */
  const int R = HB_WINDOW_BLOCKS / 2;
  for (int by = 0; by < sub_h; by++) {
    for (int bx = 0; bx < sub_w; bx++) {
      int left = bx;
      if (left < R) {
        left = R;
      }
      else if (left > sub_w - R - 1) {
        left = sub_w - R - 1;
      }
      int top = by;
      if (top < R) {
        top = R;
      }
      else if (top > sub_h - R - 1) {
        top = sub_h - R - 1;
      }
      int center = raw[(size_t)by * sub_w + bx];
      int sum = center * 2;
      int n = (center > 0) ? 2 : 0;
      for (int dy = -R; dy <= R; dy++) {
        for (int dx = -R; dx <= R; dx++) {
          int t = raw[(size_t)(top + dy) * sub_w + (left + dx)];
          sum += t;
          if (t > 0) {
            n++;
          }
        }
      }
      smoothed[(size_t)by * sub_w + bx] = (n > 0) ? (uint8_t)(sum / n) : 0;
    }
  }
  free(raw);

  /* Raster flood-fill of any remaining zero-threshold blocks. Propagates
     the nearest-preceding non-zero threshold forward; runs of trailing
     zeros inherit from the last non-zero value seen. */
  int last = -1;
  for (size_t i = 0; i < sub_n; i++) {
    if (smoothed[i]) {
      if ((int)i != last + 1) {
        uint8_t fill = smoothed[i];
        for (int j = last + 1; j < (int)i; j++) {
          smoothed[j] = fill;
        }
      }
      last = (int)i;
    }
  }
  if (last < (int)sub_n - 1) {
    uint8_t fill = (last >= 0) ? smoothed[last] : 128;
    for (int j = last + 1; j < (int)sub_n; j++) {
      smoothed[j] = fill;
    }
  }

  /* Apply per-block threshold to every pixel. Pixels in the rightmost/
     bottom partial block simply use that block's threshold — block-index
     lookup via integer division is fine even when (w % BLOCK_SIZE) != 0
     because sub_w/sub_h were computed with ceil(). */
  for (int y = 0; y < h; y++) {
    int by = y / HB_BLOCK_SIZE;
    if (by >= sub_h) {
      by = sub_h - 1;
    }
    uint8_t const *row = gray + (size_t)y * w;
    uint8_t *out = bin + (size_t)y * w;
    for (int x = 0; x < w; x++) {
      int bx = x / HB_BLOCK_SIZE;
      if (bx >= sub_w) {
        bx = sub_w - 1;
      }
      uint8_t t = smoothed[(size_t)by * sub_w + bx];
      out[x] = (row[x] <= t) ? 0 : 255;
    }
  }
  free(smoothed);
  return bin;
}

/* ---- Unsharp mask on grayscale ----
   3-tap separable Gaussian blur, then sharp = clamp(2*g - blurred). The
   small kernel matches level_4/5 blur radii (0.8-1.8 px). Returns a freshly
   allocated grayscale buffer. */
static uint8_t *unsharp_mask_gray(uint8_t const *gray, int w, int h) {
  if (w <= 2 || h <= 2) {
    return NULL;
  }
  size_t n = (size_t)w * (size_t)h;
  uint8_t *blurred = malloc(n);
  uint8_t *tmp = malloc(n);
  uint8_t *sharp = malloc(n);
  if (!blurred || !tmp || !sharp) {
    free(blurred);
    free(tmp);
    free(sharp);
    return NULL;
  }
  /* Horizontal pass [1 2 1] / 4 into tmp. */
  for (int y = 0; y < h; y++) {
    uint8_t const *row = gray + (size_t)y * w;
    uint8_t *out_row = tmp + (size_t)y * w;
    out_row[0] = row[0];
    out_row[w - 1] = row[w - 1];
    for (int x = 1; x < w - 1; x++) {
      int v = row[x - 1] + 2 * row[x] + row[x + 1];
      out_row[x] = (uint8_t)(v >> 2);
    }
  }
  /* Vertical pass [1 2 1] / 4 from tmp into blurred. */
  for (int x = 0; x < w; x++) {
    blurred[x] = tmp[x];
    blurred[(size_t)(h - 1) * w + x] = tmp[(size_t)(h - 1) * w + x];
  }
  for (int y = 1; y < h - 1; y++) {
    for (int x = 0; x < w; x++) {
      int v = tmp[(size_t)(y - 1) * w + x] + 2 * tmp[(size_t)y * w + x] +
              tmp[(size_t)(y + 1) * w + x];
      blurred[(size_t)y * w + x] = (uint8_t)(v >> 2);
    }
  }
  /* sharp = clamp(2*gray - blurred). */
  for (size_t i = 0; i < n; i++) {
    int v = 2 * (int)gray[i] - (int)blurred[i];
    if (v < 0) {
      v = 0;
    }
    if (v > 255) {
      v = 255;
    }
    sharp[i] = (uint8_t)v;
  }
  free(blurred);
  free(tmp);
  return sharp;
}

/* ---- N-x bilinear upsample on grayscale ----
   Returns a freshly allocated (N*w x N*h) buffer. Larger N gives bigger
   effective module sizes (helping finder ratio checks and subpixel
   refinement) at quadratic memory + time cost. */
static uint8_t *upsample_nx_bilinear(uint8_t const *gray, int w, int h, int n) {
  if (w <= 0 || h <= 0 || n < 2) {
    return NULL;
  }
  int w2 = w * n;
  int h2 = h * n;
  double inv = 1.0 / (double)n;
  uint8_t *out = malloc((size_t)w2 * (size_t)h2);
  if (!out) {
    return NULL;
  }
  for (int y = 0; y < h2; y++) {
    double sy = ((double)y + 0.5) * inv - 0.5;
    int y0 = (int)floor(sy);
    int y1 = y0 + 1;
    double fy = sy - y0;
    if (y0 < 0) {
      y0 = 0;
    }
    if (y1 < 0) {
      y1 = 0;
    }
    if (y0 > h - 1) {
      y0 = h - 1;
    }
    if (y1 > h - 1) {
      y1 = h - 1;
    }
    for (int x = 0; x < w2; x++) {
      double sx = ((double)x + 0.5) * inv - 0.5;
      int x0 = (int)floor(sx);
      int x1 = x0 + 1;
      double fx = sx - x0;
      if (x0 < 0) {
        x0 = 0;
      }
      if (x1 < 0) {
        x1 = 0;
      }
      if (x0 > w - 1) {
        x0 = w - 1;
      }
      if (x1 > w - 1) {
        x1 = w - 1;
      }
      double v00 = gray[y0 * w + x0];
      double v01 = gray[y0 * w + x1];
      double v10 = gray[y1 * w + x0];
      double v11 = gray[y1 * w + x1];
      double v0 = v00 * (1.0 - fx) + v01 * fx;
      double v1 = v10 * (1.0 - fx) + v11 * fx;
      double v = v0 * (1.0 - fy) + v1 * fy;
      if (v < 0.0) {
        v = 0.0;
      }
      if (v > 255.0) {
        v = 255.0;
      }
      out[y * w2 + x] = (uint8_t)(v + 0.5);
    }
  }
  return out;
}

/* ---- 2x grayscale downsample (box average) ----
   Each output pixel is the mean of a 2x2 input block. Mirrors zxing-cpp's
   LumImagePyramid step. Anti-aliased QR-edge pixels get averaged with
   interior pixels, producing new intermediate luma values within an 8x8
   block that raise its dynamic range above the hybrid binariser's
   MIN_DYNAMIC_RANGE=24 threshold. Essential for pastel palettes where
   module-vs-background contrast at full resolution is below that gate
   everywhere in the image. */
static uint8_t *downsample_2x_box(uint8_t const *gray, int w, int h) {
  if (w < 2 || h < 2) {
    return NULL;
  }
  int nw = w / 2;
  int nh = h / 2;
  uint8_t *out = malloc((size_t)nw * (size_t)nh);
  if (!out) {
    return NULL;
  }
  for (int y = 0; y < nh; y++) {
    int sy = y * 2;
    uint8_t const *r0 = gray + (size_t)sy * w;
    uint8_t const *r1 = gray + (size_t)(sy + 1) * w;
    uint8_t *dst = out + (size_t)y * nw;
    for (int x = 0; x < nw; x++) {
      int sx = x * 2;
      int sum = r0[sx] + r0[sx + 1] + r1[sx] + r1[sx + 1];
      dst[x] = (uint8_t)(sum >> 2);
    }
  }
  return out;
}

/* ---- 3x3 grayscale median filter ----
   Removes salt-and-pepper noise without softening edges (unlike Gaussian
   blur). Targets level_5's heavy random noise (sigma 4-9) that injects
   isolated black/white pixels into otherwise-uniform module interiors. */
static uint8_t *median_filter_3x3(uint8_t const *gray, int w, int h) {
  if (w <= 2 || h <= 2) {
    return NULL;
  }
  uint8_t *out = malloc((size_t)w * h);
  if (!out) {
    return NULL;
  }
  /* Copy the 1-pixel border unchanged. */
  for (int x = 0; x < w; x++) {
    out[x] = gray[x];
    out[(size_t)(h - 1) * w + x] = gray[(size_t)(h - 1) * w + x];
  }
  for (int y = 1; y < h - 1; y++) {
    out[y * w] = gray[y * w];
    out[y * w + (w - 1)] = gray[y * w + (w - 1)];
  }
  for (int y = 1; y < h - 1; y++) {
    for (int x = 1; x < w - 1; x++) {
      uint8_t v[9];
      v[0] = gray[(y - 1) * w + (x - 1)];
      v[1] = gray[(y - 1) * w + x];
      v[2] = gray[(y - 1) * w + (x + 1)];
      v[3] = gray[y * w + (x - 1)];
      v[4] = gray[y * w + x];
      v[5] = gray[y * w + (x + 1)];
      v[6] = gray[(y + 1) * w + (x - 1)];
      v[7] = gray[(y + 1) * w + x];
      v[8] = gray[(y + 1) * w + (x + 1)];
      /* Insertion sort to find the median (5th element of 9). */
      for (int i = 1; i < 9; i++) {
        uint8_t tmp = v[i];
        int j = i - 1;
        while (j >= 0 && v[j] > tmp) {
          v[j + 1] = v[j];
          j--;
        }
        v[j + 1] = tmp;
      }
      out[y * w + x] = v[4];
    }
  }
  return out;
}

/* ---- Inverted polarity ----
   QR spec allows light-on-dark modules (level_13 corpus); flatcv's finder
   and sampling code assume dark-on-light. Retrying with 255 - v lets the
   same pipeline decode both polarities without any per-stage branching. */
static uint8_t *invert_gray(uint8_t const *gray, int w, int h) {
  if (w <= 0 || h <= 0) {
    return NULL;
  }
  size_t n = (size_t)w * (size_t)h;
  uint8_t *out = malloc(n);
  if (!out) {
    return NULL;
  }
  for (size_t i = 0; i < n; i++) {
    out[i] = (uint8_t)(255 - gray[i]);
  }
  return out;
}

/* ---- Pixel access ---- */

static int is_dark(uint8_t const *px, int w, int h, int x, int y) {
  if (x < 0 || x >= w || y < 0 || y >= h) {
    return 0;
  }
  return px[y * w + x] < 128;
}

/* ---- Finder pattern ratio check (1:1:3:1:1) ---- */

static int check_ratio(const int s[5]) {
  int total = 0;
  for (int i = 0; i < 5; i++) {
    if (s[i] == 0) {
      return 0;
    }
    total += s[i];
  }
  if (total < 7) {
    return 0;
  }
  float m = (float)total / 7.0f;
  float v = m * 0.7f;
  return (
    fabsf(s[0] - m) <= v && fabsf(s[1] - m) <= v &&
    fabsf(s[2] - 3 * m) <= 3 * v && fabsf(s[3] - m) <= v && fabsf(s[4] - m) <= v
  );
}

/* ---- Cross-check along an arbitrary angle ----
   Walk along direction (cos_a, sin_a) through (cx, cy), counting the
   1:1:3:1:1 dark-light-dark-light-dark run pattern.
   Returns module_size if valid, -1 otherwise. Writes refined center. */
static float cross_check_angle(
  uint8_t const *px,
  int w,
  int h,
  float cx,
  float cy,
  float cos_a,
  float sin_a,
  int exp_total,
  float *out_cx,
  float *out_cy
) {
  int max_walk = (w > h ? w : h);

  /* Walk negative: expect dark(center), light, dark at boundary */
  int neg_dark_center = 0;
  int neg_light = 0;
  int neg_dark_outer = 0;
  int phase = 0;

  for (int step = 1; step <= max_walk && phase < 3; step++) {
    float fx = cx - step * cos_a;
    float fy = cy - step * sin_a;
    int ix = (int)(fx + 0.5f), iy = (int)(fy + 0.5f);
    if (ix < 0 || ix >= w || iy < 0 || iy >= h) {
      break;
    }
    int dark = is_dark(px, w, h, ix, iy);
    if (phase == 0) {
      if (dark) {
        neg_dark_center++;
      }
      else {
        neg_light = 1;
        phase = 1;
      }
    }
    else if (phase == 1) {
      if (!dark) {
        neg_light++;
      }
      else {
        neg_dark_outer = 1;
        phase = 2;
      }
    }
    else {
      if (dark) {
        neg_dark_outer++;
      }
      else {
        phase = 3;
      }
    }
  }

  /* Walk positive: expect dark(center), light, dark at boundary */
  int pos_dark_center = 0;
  int pos_light = 0;
  int pos_dark_outer = 0;
  phase = 0;

  for (int step = 1; step <= max_walk && phase < 3; step++) {
    float fx = cx + step * cos_a;
    float fy = cy + step * sin_a;
    int ix = (int)(fx + 0.5f), iy = (int)(fy + 0.5f);
    if (ix < 0 || ix >= w || iy < 0 || iy >= h) {
      break;
    }
    int dark = is_dark(px, w, h, ix, iy);
    if (phase == 0) {
      if (dark) {
        pos_dark_center++;
      }
      else {
        pos_light = 1;
        phase = 1;
      }
    }
    else if (phase == 1) {
      if (!dark) {
        pos_light++;
      }
      else {
        pos_dark_outer = 1;
        phase = 2;
      }
    }
    else {
      if (dark) {
        pos_dark_outer++;
      }
      else {
        phase = 3;
      }
    }
  }

  int s[5];
  s[0] = neg_dark_outer;
  s[1] = neg_light;
  s[2] = neg_dark_center + 1 + pos_dark_center;
  s[3] = pos_light;
  s[4] = pos_dark_outer;

  int total = s[0] + s[1] + s[2] + s[3] + s[4];
  if (exp_total > 0 && abs(total - exp_total) > exp_total) {
    return -1;
  }
  if (!check_ratio(s)) {
    return -1;
  }

  float offset = (float)(pos_dark_center - neg_dark_center) / 2.0f;
  *out_cx = cx + offset * cos_a;
  *out_cy = cy + offset * sin_a;

  return (float)total / 7.0f;
}

/* ---- Radial cross-check at multiple angles ---- */

static int cross_check_radial(
  uint8_t const *px,
  int w,
  int h,
  float cx,
  float cy,
  float exp_ms,
  float *out_cx,
  float *out_cy,
  float *out_ms
) {
  /* 8 angles: 0, 22.5, 45, 67.5, 90, 112.5, 135, 157.5 degrees */
  static const float angles[8] = {
    0.0f,
    0.3927f,
    0.7854f,
    1.1781f,
    1.5708f,
    1.9635f,
    2.3562f,
    2.7489f,
  };
  int exp_total = (int)(exp_ms * 7.0f + 0.5f);
  float rcx_arr[8], rcy_arr[8], rms_arr[8];
  int passed[8];
  int npass = 0;

  for (int a = 0; a < 8; a++) {
    float ca = cosf(angles[a]), sa = sinf(angles[a]);
    rms_arr[a] = cross_check_angle(
      px,
      w,
      h,
      cx,
      cy,
      ca,
      sa,
      exp_total,
      &rcx_arr[a],
      &rcy_arr[a]
    );
    passed[a] = (rms_arr[a] > 0);
    if (passed[a]) {
      npass++;
    }
  }

  if (npass < 4) {
    return 0;
  }

  int best_pair = -1;
  float best_pair_ms_match = 1e9f;
  for (int a = 0; a < 4; a++) {
    int b = a + 4;
    if (passed[a] && passed[b]) {
      float ms_err = fabsf(rms_arr[a] - exp_ms) + fabsf(rms_arr[b] - exp_ms);
      if (best_pair < 0 || ms_err < best_pair_ms_match) {
        best_pair = a;
        best_pair_ms_match = ms_err;
      }
    }
  }

  if (best_pair >= 0) {
    int a = best_pair, b = best_pair + 4;
    float ca = cosf(angles[a]), sa = sinf(angles[a]);
    float off_a = (rcx_arr[a] - cx) * ca + (rcy_arr[a] - cy) * sa;
    float cb = cosf(angles[b]), sb = sinf(angles[b]);
    float off_b = (rcx_arr[b] - cx) * cb + (rcy_arr[b] - cy) * sb;
    *out_cx = cx + off_a * ca + off_b * cb;
    *out_cy = cy + off_a * sa + off_b * sb;
    *out_ms = (rms_arr[a] + rms_arr[b]) / 2.0f;
  }
  else {
    float sum_cx = 0, sum_cy = 0, sum_ms = 0;
    for (int a = 0; a < 8; a++) {
      if (passed[a]) {
        sum_cx += rcx_arr[a];
        sum_cy += rcy_arr[a];
        sum_ms += rms_arr[a];
      }
    }
    *out_cx = sum_cx / npass;
    *out_cy = sum_cy / npass;
    *out_ms = sum_ms / npass;
  }
  return 1;
}

/* ---- Merge a candidate into the list ---- */

static int merge_candidate(
  FinderPattern *cands,
  int nc,
  int max_cands,
  float cx,
  float cy,
  float ms
) {
  float merge_r = ms * 1.5f;
  for (int c = 0; c < nc; c++) {
    float dx = cands[c].x - cx;
    float dy = cands[c].y - cy;
    if (sqrtf(dx * dx + dy * dy) < merge_r) {
      cands[c].x = (cands[c].x + cx) / 2;
      cands[c].y = (cands[c].y + cy) / 2;
      cands[c].module_size = (cands[c].module_size + ms) / 2;
      return nc;
    }
  }
  if (nc < max_cands) {
    cands[nc].x = cx;
    cands[nc].y = cy;
    cands[nc].module_size = ms;
    return nc + 1;
  }
  return nc;
}

/* ---- Scan a single line of pixels for finder patterns ---- */

typedef struct {
  uint8_t const *px;
  int w, h;
  int x0, y0, dx, dy, len;
} ScanLine;

static int get_dark_at(const ScanLine *sl, int step) {
  int x = sl->x0 + step * sl->dx;
  int y = sl->y0 + step * sl->dy;
  return is_dark(sl->px, sl->w, sl->h, x, y);
}

static void scan_one_line(
  const ScanLine *sl,
  FinderPattern *cands,
  int *nc,
  int max_cands
) {
  int s[5] = {0};
  int st = 0;

  for (int i = 0; i < sl->len; i++) {
    int dark = get_dark_at(sl, i);
    if (dark) {
      if (st == 1 || st == 3) {
        st++;
      }
      s[st]++;
    }
    else {
      if (st == 0 || st == 2) {
        st++;
        s[st]++;
      }
      else if (st == 4) {
        if (check_ratio(s)) {
          int total = s[0] + s[1] + s[2] + s[3] + s[4];
          float center_step = (float)(i - s[4] - s[3]) - (float)s[2] / 2.0f;
          float cx = sl->x0 + center_step * sl->dx;
          float cy = sl->y0 + center_step * sl->dy;
          float ms = (float)total / 7.0f;

          float rcx, rcy, rms;
          if (cross_check_radial(
                sl->px,
                sl->w,
                sl->h,
                cx,
                cy,
                ms,
                &rcx,
                &rcy,
                &rms
              )) {
            *nc = merge_candidate(cands, *nc, max_cands, rcx, rcy, rms);
          }
        }
        s[0] = s[2];
        s[1] = s[3];
        s[2] = s[4];
        s[3] = 1;
        s[4] = 0;
        st = 3;
      }
      else {
        s[st]++;
      }
    }
  }
}

/* ---- Find finder patterns with multi-direction scanning ---- */

static int
find_finders(uint8_t const *px, int w, int h, FinderPattern *out, int max_out) {
  FinderPattern cands[MAX_CANDIDATES];
  int nc = 0;
  ScanLine sl;
  sl.px = px;
  sl.w = w;
  sl.h = h;

  /* Direction 1: Horizontal */
  sl.dx = 1;
  sl.dy = 0;
  for (int y = 0; y < h && nc < MAX_CANDIDATES; y++) {
    sl.x0 = 0;
    sl.y0 = y;
    sl.len = w;
    scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
  }

  /* Direction 2: Vertical */
  sl.dx = 0;
  sl.dy = 1;
  for (int x = 0; x < w && nc < MAX_CANDIDATES; x++) {
    sl.x0 = x;
    sl.y0 = 0;
    sl.len = h;
    scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
  }

  /* Direction 3: Diagonal 45° */
  sl.dx = 1;
  sl.dy = 1;
  for (int y = 0; y < h && nc < MAX_CANDIDATES; y++) {
    sl.x0 = 0;
    sl.y0 = y;
    sl.len = (w < h - y) ? w : h - y;
    scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
  }
  for (int x = 1; x < w && nc < MAX_CANDIDATES; x++) {
    sl.x0 = x;
    sl.y0 = 0;
    sl.len = (w - x < h) ? w - x : h;
    scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
  }

  /* Direction 4: Diagonal 135° */
  sl.dx = 1;
  sl.dy = -1;
  for (int y = 0; y < h && nc < MAX_CANDIDATES; y++) {
    sl.x0 = 0;
    sl.y0 = y;
    sl.len = (w < y + 1) ? w : y + 1;
    scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
  }
  for (int x = 1; x < w && nc < MAX_CANDIDATES; x++) {
    sl.x0 = x;
    sl.y0 = h - 1;
    sl.len = (w - x < h) ? w - x : h;
    scan_one_line(&sl, cands, &nc, MAX_CANDIDATES);
  }

  int n = (nc < max_out) ? nc : max_out;
  for (int i = 0; i < n; i++) {
    out[i] = cands[i];
  }
  return n;
}

/* ---- Subpixel finder center refinement ----
   Once a candidate center is detected, refine it by computing the weighted
   centroid of dark pixels in the inner 3x3-module dark square. This fixes
   the ±2-3 pixel systematic bias from integer-pixel 1D scans, which
   otherwise throws off grid sampling under perspective distortion. */
static void
refine_finder_center(uint8_t const *gray, int w, int h, FinderPattern *fp) {
  float ms = fp->module_size;
  if (ms < 1.0f) {
    return;
  }
  /* Search in a 3-module window centered on the current estimate so the
     centroid is dominated by the inner 3x3 dark core and not the outer
     7x7 dark ring (which would pull the result off-center). */
  int r = (int)(ms * 1.5f);
  int x0 = (int)(fp->x + 0.5f) - r;
  int x1 = (int)(fp->x + 0.5f) + r;
  int y0 = (int)(fp->y + 0.5f) - r;
  int y1 = (int)(fp->y + 0.5f) + r;
  if (x0 < 0) {
    x0 = 0;
  }
  if (y0 < 0) {
    y0 = 0;
  }
  if (x1 >= w) {
    x1 = w - 1;
  }
  if (y1 >= h) {
    y1 = h - 1;
  }
  if (x1 <= x0 || y1 <= y0) {
    return;
  }
  /* Compute local min/max to threshold adaptively within this tiny window. */
  uint8_t lo = 255, hi = 0;
  for (int y = y0; y <= y1; y++) {
    for (int x = x0; x <= x1; x++) {
      uint8_t v = gray[y * w + x];
      if (v < lo) {
        lo = v;
      }
      if (v > hi) {
        hi = v;
      }
    }
  }
  if (hi <= lo + 10) {
    return; /* no contrast — leave center alone */
  }
  int t = (lo + hi) / 2;
  double sx = 0, sy = 0, sw = 0;
  for (int y = y0; y <= y1; y++) {
    for (int x = x0; x <= x1; x++) {
      uint8_t v = gray[y * w + x];
      if (v < t) {
        double weight = (double)(t - v);
        sx += (double)x * weight;
        sy += (double)y * weight;
        sw += weight;
      }
    }
  }
  if (sw > 0) {
    fp->x = (float)(sx / sw);
    fp->y = (float)(sy / sw);
  }
}

/* ---- Identify TL, TR, BL ---- */

static float fp_dist(FinderPattern a, FinderPattern b) {
  float dx = a.x - b.x, dy = a.y - b.y;
  return sqrtf(dx * dx + dy * dy);
}

static void identify_triple(
  FinderPattern a,
  FinderPattern b,
  FinderPattern c,
  FinderPattern *tl,
  FinderPattern *tr,
  FinderPattern *bl
) {
  *tl = a;
  float cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  if (cross > 0) {
    *tr = b;
    *bl = c;
  }
  else {
    *tr = c;
    *bl = b;
  }
}

/* ---- Timing pattern verification ----
   Walk the candidate timing row between two finder centers, offset 3
   modules perpendicular toward the third finder (finder center is at
   module row 3, timing pattern at module row 6). A real QR timing row
   shows alternating dark/light modules at module-size pitch; incidental
   1:1:3:1:1 "finder" triples in random data do not. Returns transitions
   per step in [0,1]; ≥ 0.75 reliably indicates a timing pattern (random
   data averages 0.5 with 95% CI up to ~0.67). */
static float timing_score_pair(
  uint8_t const *px,
  int w,
  int h,
  FinderPattern a,
  FinderPattern b,
  FinderPattern c_toward
) {
  float ms = (a.module_size + b.module_size) / 2.0f;
  if (ms < 1.0f) {
    return 0;
  }
  float ux = b.x - a.x, uy = b.y - a.y;
  float u_len = sqrtf(ux * ux + uy * uy);
  if (u_len < 10.0f * ms) {
    return 0;
  }
  ux /= u_len;
  uy /= u_len;
  float perp_x = -uy, perp_y = ux;
  float to_c_x = c_toward.x - a.x, to_c_y = c_toward.y - a.y;
  if (perp_x * to_c_x + perp_y * to_c_y < 0) {
    perp_x = -perp_x;
    perp_y = -perp_y;
  }
  /* Finder center is the center of module (3,3); timing pattern runs along
     the center of module (·,6). Offset from finder-center line to timing
     line is 6 − 3 = 3 modules. */
  float offset_x = 3.0f * ms * perp_x;
  float offset_y = 3.0f * ms * perp_y;
  float sx = a.x + 4.0f * ms * ux + offset_x;
  float sy = a.y + 4.0f * ms * uy + offset_y;
  float ex = b.x - 4.0f * ms * ux + offset_x;
  float ey = b.y - 4.0f * ms * uy + offset_y;
  float walk_len = sqrtf((ex - sx) * (ex - sx) + (ey - sy) * (ey - sy));
  int n_steps = (int)(walk_len / ms + 0.5f);
  if (n_steps < 6 || n_steps > 165) {
    return 0;
  }
  int prev_dark = -1;
  int transitions = 0;
  int samples = 0;
  for (int i = 0; i <= n_steps; i++) {
    float t = (float)i / (float)n_steps;
    float fx = sx + t * (ex - sx);
    float fy = sy + t * (ey - sy);
    int ix = (int)(fx + 0.5f), iy = (int)(fy + 0.5f);
    if (ix < 0 || ix >= w || iy < 0 || iy >= h) {
      continue;
    }
    int dark = is_dark(px, w, h, ix, iy);
    samples++;
    if (prev_dark != -1 && dark != prev_dark) {
      transitions++;
    }
    prev_dark = dark;
  }
  if (samples < 5) {
    return 0;
  }
  return (float)transitions / (float)(samples - 1);
}

static int select_and_rank(
  uint8_t const *px,
  int w,
  int h,
  FinderPattern *fps,
  int nfp,
  FinderPattern triples[][3],
  int max_triples
) {
  if (nfp < 3) {
    return 0;
  }

  /* Count ms-cluster partners for each candidate: how many other candidates
     have module_size within 15% of mine. Real finder triples are three
     near-identical-ms blobs; scoring-by-ms alone lets wide-run false
     positives in dense data regions push the real triple out of the
     search window on cluttered images (e.g. receipt QR next to a
     barcode). Clustered candidates get promoted so the triple survives. */
  int cluster_count[MAX_CANDIDATES];
  for (int i = 0; i < nfp; i++) {
    int count = 0;
    for (int j = 0; j < nfp; j++) {
      if (j == i) {
        continue;
      }
      float m = (fps[i].module_size > 0.001f) ? fps[i].module_size : 0.001f;
      float ratio = fabsf(fps[i].module_size - fps[j].module_size) / m;
      if (ratio < 0.15f) {
        count++;
      }
    }
    cluster_count[i] = count;
  }

  /* Selection sort by (clustered desc, module_size desc). Candidates with
     ≥ 2 ms-partners come first; within each group, larger ms wins. */
  for (int i = 0; i < nfp - 1; i++) {
    int best = i;
    for (int j = i + 1; j < nfp; j++) {
      int j_cl = cluster_count[j] >= 2;
      int b_cl = cluster_count[best] >= 2;
      int replace = 0;
      if (j_cl && !b_cl) {
        replace = 1;
      }
      else if (j_cl == b_cl && fps[j].module_size > fps[best].module_size) {
        replace = 1;
      }
      if (replace) {
        best = j;
      }
    }
    if (best != i) {
      FinderPattern tmp = fps[i];
      fps[i] = fps[best];
      fps[best] = tmp;
      int tc = cluster_count[i];
      cluster_count[i] = cluster_count[best];
      cluster_count[best] = tc;
    }
  }

  typedef struct {
    float score;
    int i, j, k;
  } Scored;
  int max_scored = max_triples * 4;
  if (max_scored > 200) {
    max_scored = 200;
  }
  Scored *scored = malloc(max_scored * sizeof(Scored));
  if (!scored) {
    return 0;
  }
  int ns = 0;

  /* Consider all clustered candidates (capped at 80 for O(n³) budget), or at
     least 30 if few candidates cluster — preserves original behaviour on
     noise-only scenes while accommodating crowded ones. */
  int num_clustered = 0;
  for (int i = 0; i < nfp; i++) {
    if (cluster_count[i] >= 2) {
      num_clustered++;
    }
  }
  int limit = num_clustered;
  if (limit < 30) {
    limit = 30;
  }
  if (limit > nfp) {
    limit = nfp;
  }
  if (limit > 80) {
    limit = 80;
  }
  for (int i = 0; i < limit; i++) {
    for (int j = i + 1; j < limit; j++) {
      for (int k = j + 1; k < limit; k++) {
        float ms_avg =
          (fps[i].module_size + fps[j].module_size + fps[k].module_size) / 3.0f;
        float ms_var = (fabsf(fps[i].module_size - ms_avg) +
                        fabsf(fps[j].module_size - ms_avg) +
                        fabsf(fps[k].module_size - ms_avg)) /
                       ms_avg;

        float d01 = fp_dist(fps[i], fps[j]);
        float d02 = fp_dist(fps[i], fps[k]);
        float d12 = fp_dist(fps[j], fps[k]);

        float sides[3] = {d01, d02, d12};
        for (int a = 0; a < 2; a++) {
          for (int b = a + 1; b < 3; b++) {
            if (sides[a] > sides[b]) {
              float t = sides[a];
              sides[a] = sides[b];
              sides[b] = t;
            }
          }
        }

        float side_ratio = (sides[1] > 0) ? sides[0] / sides[1] : 0;
        float expected_hyp = sqrtf(sides[0] * sides[0] + sides[1] * sides[1]);
        float hyp_err = (expected_hyp > 0)
                          ? fabsf(sides[2] - expected_hyp) / expected_hyp
                          : 1;

        /* Real QR finder centers sit at module (3.5, 3.5), (sz-3.5, 3.5),
           (3.5, sz-3.5) — so the short side divided by module size should be
           qr_size - 7, at least 14 for v1. Triples formed from incidental
           1:1:3:1:1 patterns in adjacent data modules have small
           sides/ms ratios; saturate the bonus at 14 so real finders outrank
           them without giving oversized triples unbounded advantage. */
        float modules_between = (ms_avg > 0) ? sides[0] / ms_avg : 0;
        /* QR spec: finder-center distance = (qr_size − 7) × module_size, with
           qr_size ∈ [21, 177]. So the longer leg cannot plausibly exceed
           ~170 modules; leave ~10% slack for perspective and scan noise. A
           triple spanning an entire receipt/page at some spurious module size
           blows past this even when its module sizes happen to cluster. */
        float modules_between_long = (ms_avg > 0) ? sides[1] / ms_avg : 0;
        if (modules_between_long > 190.0f) {
          continue;
        }
        float scale_bonus = modules_between / 14.0f;
        if (scale_bonus > 1.0f) {
          scale_bonus = 1.0f;
        }
        /* ms_var dominates: real QR finders have near-identical module sizes;
           triples whose "finders" are false positives at different scales
           get rejected hard. Discount hyp_err so moderate perspective
           distortion doesn't kill the true triple. */
        float score = side_ratio - ms_var * 5.0f - hyp_err * 1.0f + scale_bonus;

        /* Timing-pattern bonus: real finder triples have two pairs whose
           inter-finder line runs along a QR timing row of alternating
           dark/light modules. False-positive triples in dense document/
           receipt text rarely hit this, so the bonus of +1 per timing
           pair lets perspective-distorted real triples outrank clean but
           incidental right-triangle triples. Gate on threshold 0.75 —
           random data ratio is ≤ 0.67 with 95% confidence. */
        int timing_pairs = 0;
        float t_ij = timing_score_pair(px, w, h, fps[i], fps[j], fps[k]);
        float t_ik = timing_score_pair(px, w, h, fps[i], fps[k], fps[j]);
        float t_jk = timing_score_pair(px, w, h, fps[j], fps[k], fps[i]);
        if (t_ij >= 0.75f) {
          timing_pairs++;
        }
        if (t_ik >= 0.75f) {
          timing_pairs++;
        }
        if (t_jk >= 0.75f) {
          timing_pairs++;
        }
        score += (float)timing_pairs;

        if (ns < max_scored) {
          scored[ns++] = (Scored){score, i, j, k};
        }
        else {
          int worst = 0;
          for (int s = 1; s < ns; s++) {
            if (scored[s].score < scored[worst].score) {
              worst = s;
            }
          }
          if (score > scored[worst].score) {
            scored[worst] = (Scored){score, i, j, k};
          }
        }
      }
    }
  }

  for (int i = 0; i < ns - 1; i++) {
    for (int j = i + 1; j < ns; j++) {
      if (scored[j].score > scored[i].score) {
        Scored t = scored[i];
        scored[i] = scored[j];
        scored[j] = t;
      }
    }
  }

  int out_count = (ns < max_triples) ? ns : max_triples;
  for (int t = 0; t < out_count; t++) {
    FinderPattern triple[3] =
      {fps[scored[t].i], fps[scored[t].j], fps[scored[t].k]};
    float d01 = fp_dist(triple[0], triple[1]);
    float d02 = fp_dist(triple[0], triple[2]);
    float d12 = fp_dist(triple[1], triple[2]);
    FinderPattern a, b, c;
    if (d01 >= d02 && d01 >= d12) {
      a = triple[2];
      b = triple[0];
      c = triple[1];
    }
    else if (d02 >= d01 && d02 >= d12) {
      a = triple[1];
      b = triple[0];
      c = triple[2];
    }
    else {
      a = triple[0];
      b = triple[1];
      c = triple[2];
    }
    identify_triple(a, b, c, &triples[t][0], &triples[t][1], &triples[t][2]);
  }

  free(scored);
  return out_count;
}

/* ---- Homography (perspective) ---- */

/* Solve an 8x8 linear system for a perspective transform that maps 4 source
   points to 4 destination points. Returns 1 on success, 0 if singular. */
static int compute_homography(
  const double src[4][2],
  const double dst[4][2],
  double H[9]
) {
  double A[8][9];
  memset(A, 0, sizeof(A));
  for (int i = 0; i < 4; i++) {
    double sx = src[i][0], sy = src[i][1];
    double dx = dst[i][0], dy = dst[i][1];
    A[2 * i][0] = sx;
    A[2 * i][1] = sy;
    A[2 * i][2] = 1.0;
    A[2 * i][6] = -sx * dx;
    A[2 * i][7] = -sy * dx;
    A[2 * i][8] = dx;
    A[2 * i + 1][3] = sx;
    A[2 * i + 1][4] = sy;
    A[2 * i + 1][5] = 1.0;
    A[2 * i + 1][6] = -sx * dy;
    A[2 * i + 1][7] = -sy * dy;
    A[2 * i + 1][8] = dy;
  }
  for (int i = 0; i < 8; i++) {
    int max_row = i;
    double max_val = fabs(A[i][i]);
    for (int k = i + 1; k < 8; k++) {
      if (fabs(A[k][i]) > max_val) {
        max_val = fabs(A[k][i]);
        max_row = k;
      }
    }
    if (max_val < 1e-10) {
      return 0;
    }
    if (max_row != i) {
      for (int j = 0; j <= 8; j++) {
        double t = A[i][j];
        A[i][j] = A[max_row][j];
        A[max_row][j] = t;
      }
    }
    for (int j = i + 1; j < 8; j++) {
      double f = A[j][i] / A[i][i];
      for (int k = i; k <= 8; k++) {
        A[j][k] -= f * A[i][k];
      }
    }
  }
  for (int i = 7; i >= 0; i--) {
    if (fabs(A[i][i]) < 1e-10) {
      return 0;
    }
    double s = A[i][8];
    for (int j = i + 1; j < 8; j++) {
      s -= A[i][j] * H[j];
    }
    H[i] = s / A[i][i];
    if (isnan(H[i]) || isinf(H[i])) {
      return 0;
    }
  }
  H[8] = 1.0;
  return 1;
}

static void apply_homography(
  const double H[9],
  double mx,
  double my,
  float *out_x,
  float *out_y
) {
  double w = H[6] * mx + H[7] * my + H[8];
  if (fabs(w) < 1e-10) {
    *out_x = 0;
    *out_y = 0;
    return;
  }
  double x = (H[0] * mx + H[1] * my + H[2]) / w;
  double y = (H[3] * mx + H[4] * my + H[5]) / w;
  /* Clamp to a range safely castable to int by callers. Values outside this
     range come from ill-conditioned homographies and would be off-image
     anyway; unclamped they trigger UB when cast via (int)fx. */
  const double lim = 1.0e7;
  if (!(x == x) || x < -lim) {
    x = -lim;
  }
  else if (x > lim) {
    x = lim;
  }
  if (!(y == y) || y < -lim) {
    y = -lim;
  }
  else if (y > lim) {
    y = lim;
  }
  *out_x = (float)x;
  *out_y = (float)y;
}

/* Solve the linear system Mh = v where M is 8x8 symmetric (A^T A) and v is
   8x1 (A^T b), by Gaussian elimination with partial pivoting. Writes the
   result into the first 8 slots of H and sets H[8] = 1. Returns 1 on
   success, 0 on singular / ill-conditioned input. */
static int solve_8x8(double M[8][8], const double v[8], double H[9]) {
  double A[8][9];
  for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 8; j++) {
      A[i][j] = M[i][j];
    }
    A[i][8] = v[i];
  }
  for (int i = 0; i < 8; i++) {
    int max_row = i;
    double max_val = fabs(A[i][i]);
    for (int k = i + 1; k < 8; k++) {
      if (fabs(A[k][i]) > max_val) {
        max_val = fabs(A[k][i]);
        max_row = k;
      }
    }
    if (max_val < 1e-12) {
      return 0;
    }
    if (max_row != i) {
      for (int j = 0; j <= 8; j++) {
        double t = A[i][j];
        A[i][j] = A[max_row][j];
        A[max_row][j] = t;
      }
    }
    for (int k = i + 1; k < 8; k++) {
      double f = A[k][i] / A[i][i];
      for (int j = i; j <= 8; j++) {
        A[k][j] -= f * A[i][j];
      }
    }
  }
  for (int i = 7; i >= 0; i--) {
    if (fabs(A[i][i]) < 1e-12) {
      return 0;
    }
    double s = A[i][8];
    for (int j = i + 1; j < 8; j++) {
      s -= A[i][j] * H[j];
    }
    H[i] = s / A[i][i];
    if (isnan(H[i]) || isinf(H[i])) {
      return 0;
    }
  }
  H[8] = 1.0;
  return 1;
}

/* Solve an over-determined homography system from N≥4 correspondences via
   normal equations: the 2N×8 constraint matrix A is folded into an 8×8
   symmetric M = A^T A and an 8×1 vector v = A^T b, and the 8×8 system is
   solved directly. No Hartley normalisation — QR decoding stays within a
   few hundred pixels so the coordinate magnitudes don't blow out double
   precision here. Returns 1 on success, 0 on singular input. */
static int compute_homography_ls(
  const double src[][2],
  const double dst[][2],
  int n,
  double H[9]
) {
  if (n < 4) {
    return 0;
  }
  double M[8][8];
  double v[8];
  memset(M, 0, sizeof(M));
  memset(v, 0, sizeof(v));

  for (int i = 0; i < n; i++) {
    double X = src[i][0], Y = src[i][1];
    double x = dst[i][0], y = dst[i][1];

    /* x-equation row: [X, Y, 1, 0, 0, 0, -x*X, -x*Y] with RHS = x */
    double rx[8] = {X, Y, 1, 0, 0, 0, -x * X, -x * Y};
    for (int a = 0; a < 8; a++) {
      for (int b = 0; b < 8; b++) {
        M[a][b] += rx[a] * rx[b];
      }
      v[a] += rx[a] * x;
    }

    /* y-equation row: [0, 0, 0, X, Y, 1, -y*X, -y*Y] with RHS = y */
    double ry[8] = {0, 0, 0, X, Y, 1, -y * X, -y * Y};
    for (int a = 0; a < 8; a++) {
      for (int b = 0; b < 8; b++) {
        M[a][b] += ry[a] * ry[b];
      }
      v[a] += ry[a] * y;
    }
  }

  return solve_8x8(M, v, H);
}

/* Solve an NxN linear system M x = v by Gaussian elimination with partial
   pivoting. M is destroyed. Result written into x. Returns 1 on success,
   0 on singular matrix. */
static int solve_nxn(double *M, double *v, int n, double *x) {
  /* Augment [M | v] into A: A is n × (n+1), row-major. */
  int stride = n + 1;
  double *A = (double *)malloc((size_t)n * (size_t)stride * sizeof(double));
  if (!A) {
    return 0;
  }
  for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
      A[i * stride + j] = M[i * n + j];
    }
    A[i * stride + n] = v[i];
  }
  for (int i = 0; i < n; i++) {
    int max_row = i;
    double max_val = fabs(A[i * stride + i]);
    for (int k = i + 1; k < n; k++) {
      double av = fabs(A[k * stride + i]);
      if (av > max_val) {
        max_val = av;
        max_row = k;
      }
    }
    if (max_val < 1e-12) {
      free(A);
      return 0;
    }
    if (max_row != i) {
      for (int j = 0; j <= n; j++) {
        double t = A[i * stride + j];
        A[i * stride + j] = A[max_row * stride + j];
        A[max_row * stride + j] = t;
      }
    }
    for (int k = i + 1; k < n; k++) {
      double f = A[k * stride + i] / A[i * stride + i];
      for (int j = i; j <= n; j++) {
        A[k * stride + j] -= f * A[i * stride + j];
      }
    }
  }
  for (int i = n - 1; i >= 0; i--) {
    if (fabs(A[i * stride + i]) < 1e-12) {
      free(A);
      return 0;
    }
    double s = A[i * stride + n];
    for (int j = i + 1; j < n; j++) {
      s -= A[i * stride + j] * x[j];
    }
    x[i] = s / A[i * stride + i];
    if (isnan(x[i]) || isinf(x[i])) {
      free(A);
      return 0;
    }
  }
  free(A);
  return 1;
}

/* Fit a quadratic warp (u, v) -> (x, y) using N correspondences. The warp
   is a bivariate polynomial of degree 2 per output coordinate, i.e.
       x(u,v) = a0 + a1·u + a2·v + a3·u·v + a4·u² + a5·v²
       y(u,v) = b0 + b1·u + b2·v + b3·u·v + b4·u² + b5·v²
   — 12 parameters fit via independent 6-unknown normal equations per
   coordinate. Needs ≥ 6 observations and they must span a 2-D region
   (all points on one line gives a singular matrix). Writes 12 floats
   into coeffs: indices 0..5 are the x-coefficients in the order above,
   6..11 are the y-coefficients. Returns 1 on success, 0 on failure.

   A quadratic warp can model paper curl and moderate lens/fisheye
   distortion that a single 8-parameter homography can't. It's not
   perspective-correct for truly flat surfaces, but on real-world
   photographs of QR codes on non-planar surfaces (receipts, folded
   paper, curved displays) it typically gives sub-pixel accuracy where
   the homography drifts by 1–2 modules across the data area. */
static int fit_quadratic_warp(
  const double src[][2],
  const double dst[][2],
  int n,
  double coeffs[12]
) {
  if (n < 6) {
    return 0;
  }
  double Mx[36], vx[6];
  double My[36], vy[6];
  memset(Mx, 0, sizeof(Mx));
  memset(My, 0, sizeof(My));
  memset(vx, 0, sizeof(vx));
  memset(vy, 0, sizeof(vy));
  for (int i = 0; i < n; i++) {
    double u = src[i][0], v = src[i][1];
    double x = dst[i][0], y = dst[i][1];
    double row[6] = {1.0, u, v, u * v, u * u, v * v};
    for (int a = 0; a < 6; a++) {
      for (int b = 0; b < 6; b++) {
        Mx[a * 6 + b] += row[a] * row[b];
        My[a * 6 + b] += row[a] * row[b];
      }
      vx[a] += row[a] * x;
      vy[a] += row[a] * y;
    }
  }
  double cx[6] = {0}, cy[6] = {0};
  if (!solve_nxn(Mx, vx, 6, cx) || !solve_nxn(My, vy, 6, cy)) {
    return 0;
  }
  for (int i = 0; i < 6; i++) {
    coeffs[i] = cx[i];
    coeffs[i + 6] = cy[i];
  }
  return 1;
}

/* Apply a quadratic warp to map a (u, v) module-space point to a (px, py)
   pixel-space point. Matches the coefficient layout of fit_quadratic_warp. */
static void apply_quadratic_warp(
  const double coeffs[12],
  double u,
  double v,
  double *px,
  double *py
) {
  *px = coeffs[0] + coeffs[1] * u + coeffs[2] * v + coeffs[3] * u * v +
        coeffs[4] * u * u + coeffs[5] * v * v;
  *py = coeffs[6] + coeffs[7] * u + coeffs[8] * v + coeffs[9] * u * v +
        coeffs[10] * u * u + coeffs[11] * v * v;
}

/* Light-pixel analogue of find_dark_centroid_local — centroid of *light*
   pixels in a local window. Used for known-light QR cells (e.g. the light
   timing modules, the light 3x3 interior of the finder pattern) to add
   anchor points between the dark modules the primary observer finds. */
static int find_light_centroid_local(
  uint8_t const *gray,
  int w,
  int h,
  double px_pred,
  double py_pred,
  float search_r,
  double *px_obs,
  double *py_obs
) {
  int r = (int)(search_r + 0.5f);
  if (r < 2) {
    r = 2;
  }
  int cx = (int)(px_pred + 0.5);
  int cy = (int)(py_pred + 0.5);
  int x0 = cx - r;
  int y0 = cy - r;
  int x1 = cx + r;
  int y1 = cy + r;
  if (x0 < 0) {
    x0 = 0;
  }
  if (y0 < 0) {
    y0 = 0;
  }
  if (x1 >= w) {
    x1 = w - 1;
  }
  if (y1 >= h) {
    y1 = h - 1;
  }
  if (x1 - x0 < 2 || y1 - y0 < 2) {
    return 0;
  }
  uint8_t lo = 255, hi = 0;
  for (int y = y0; y <= y1; y++) {
    for (int x = x0; x <= x1; x++) {
      uint8_t g = gray[y * w + x];
      if (g < lo) {
        lo = g;
      }
      if (g > hi) {
        hi = g;
      }
    }
  }
  if (hi - lo < 20) {
    return 0;
  }
  int t = (lo + hi) / 2;
  double sx = 0, sy = 0, sw = 0;
  for (int y = y0; y <= y1; y++) {
    for (int x = x0; x <= x1; x++) {
      uint8_t g = gray[y * w + x];
      if (g >= t) {
        double weight = (double)(g - t);
        sx += (double)x * weight;
        sy += (double)y * weight;
        sw += weight;
      }
    }
  }
  if (sw <= 0) {
    return 0;
  }
  *px_obs = sx / sw;
  *py_obs = sy / sw;
  return 1;
}

/* Observe the pixel-space centroid of dark pixels in a local window around
   a predicted position, using an adaptive local threshold. Used to ground-
   truth the position of a known-dark QR module (timing pattern dark
   module, alignment center, etc.) against the pixel buffer so we can
   refine a homography that was built from far-apart control points.
   Returns 1 on success; 0 if the window is uniform (low contrast ⇒
   unreliable). */
static int find_dark_centroid_local(
  uint8_t const *gray,
  int w,
  int h,
  double px_pred,
  double py_pred,
  float search_r,
  double *px_obs,
  double *py_obs
) {
  int r = (int)(search_r + 0.5f);
  if (r < 2) {
    r = 2;
  }
  int cx = (int)(px_pred + 0.5);
  int cy = (int)(py_pred + 0.5);
  int x0 = cx - r;
  int y0 = cy - r;
  int x1 = cx + r;
  int y1 = cy + r;
  if (x0 < 0) {
    x0 = 0;
  }
  if (y0 < 0) {
    y0 = 0;
  }
  if (x1 >= w) {
    x1 = w - 1;
  }
  if (y1 >= h) {
    y1 = h - 1;
  }
  if (x1 - x0 < 2 || y1 - y0 < 2) {
    return 0;
  }
  uint8_t lo = 255, hi = 0;
  for (int y = y0; y <= y1; y++) {
    for (int x = x0; x <= x1; x++) {
      uint8_t g = gray[y * w + x];
      if (g < lo) {
        lo = g;
      }
      if (g > hi) {
        hi = g;
      }
    }
  }
  if (hi - lo < 20) {
    return 0;
  }
  int t = (lo + hi) / 2;
  double sx = 0, sy = 0, sw = 0;
  for (int y = y0; y <= y1; y++) {
    for (int x = x0; x <= x1; x++) {
      uint8_t g = gray[y * w + x];
      if (g < t) {
        double weight = (double)(t - g);
        sx += (double)x * weight;
        sy += (double)y * weight;
        sw += weight;
      }
    }
  }
  if (sw <= 0) {
    return 0;
  }
  *px_obs = sx / sw;
  *py_obs = sy / sw;
  return 1;
}

/* Forward declarations — defined later alongside the homography decode path. */
static char *decode_grid(
  uint8_t *grid,
  uint8_t const *conf,
  int qr_size,
  int version,
  int *out_fmt_dist,
  int *out_rs_cost
);
static float timing_error_rate(uint8_t const *grid, int qr_size);
static float finder_error_rate(uint8_t const *grid, int qr_size);

/* Sample the QR module grid using a quadratic warp instead of a homography.
   Behaves like sample_grid_homography_offset_mode's non-bilinear path but
   with a polynomial mapping that can bend to follow paper curl. The 5-
   point sub-sample voting is preserved for tolerance against small
   within-module drift. Returns a heap buffer of size qr_size*qr_size;
   caller frees. */
static uint8_t *sample_grid_quadratic(
  uint8_t const *px,
  int w,
  int h,
  const double coeffs[12],
  int qr_size
) {
  int total = qr_size * qr_size;
  uint8_t *grid = calloc(total, 1);
  if (!grid) {
    return NULL;
  }
  static const double offs[5][2] = {
    {0.5, 0.5},
    {0.35, 0.35},
    {0.65, 0.35},
    {0.35, 0.65},
    {0.65, 0.65},
  };
  for (int r = 0; r < qr_size; r++) {
    for (int c = 0; c < qr_size; c++) {
      int votes = 0;
      for (int o = 0; o < 5; o++) {
        double px_d, py_d;
        apply_quadratic_warp(
          coeffs,
          c + offs[o][0],
          r + offs[o][1],
          &px_d,
          &py_d
        );
        int ix = (int)(px_d + 0.5);
        int iy = (int)(py_d + 0.5);
        if (is_dark(px, w, h, ix, iy)) {
          votes++;
        }
      }
      grid[r * qr_size + c] = (votes >= 3) ? 1 : 0;
    }
  }
  return grid;
}

/* Fit a quadratic warp from 3 finder centers + alignment + all observable
   timing-module centroids, then sample the grid with it and try to
   decode. Returns a heap-allocated decoded string (caller frees) or
   NULL if sampling/decode fails. */
static char *try_decode_quadratic(
  uint8_t const *bin,
  uint8_t const *gray,
  int w,
  int h,
  int qr_size,
  int version,
  FinderPattern tl,
  FinderPattern tr,
  FinderPattern bl,
  int have_alignment,
  double alignment_x,
  double alignment_y,
  const double H_init[9],
  float ms,
  int *out_fmt_dist,
  int *out_rs_cost
) {
  enum { MAX_CORR = 80 };
  double src[MAX_CORR][2];
  double dst[MAX_CORR][2];
  int n = 0;
  src[n][0] = 3.5;
  src[n][1] = 3.5;
  dst[n][0] = tl.x;
  dst[n][1] = tl.y;
  n++;
  src[n][0] = qr_size - 3.5;
  src[n][1] = 3.5;
  dst[n][0] = tr.x;
  dst[n][1] = tr.y;
  n++;
  src[n][0] = 3.5;
  src[n][1] = qr_size - 3.5;
  dst[n][0] = bl.x;
  dst[n][1] = bl.y;
  n++;
  if (have_alignment) {
    src[n][0] = qr_size - 6.5;
    src[n][1] = qr_size - 6.5;
    dst[n][0] = alignment_x;
    dst[n][1] = alignment_y;
    n++;
  }
  float search_r = ms * 0.5f;
  /* Horizontal timing pattern: dark at even cols, light at odd cols. Sample
     both so the quadratic warp has fine-grained constraints along row 6. */
  for (int col = 7; col <= qr_size - 8; col++) {
    if (n >= MAX_CORR) {
      break;
    }
    double mx = (double)col + 0.5;
    double my = 6.5;
    double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
    if (fabs(pw) < 1e-10) {
      continue;
    }
    double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
    double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
    double px_obs, py_obs;
    int ok_c;
    if ((col % 2) == 0) {
      ok_c = find_dark_centroid_local(
        gray,
        w,
        h,
        px_pred,
        py_pred,
        search_r,
        &px_obs,
        &py_obs
      );
    }
    else {
      ok_c = find_light_centroid_local(
        gray,
        w,
        h,
        px_pred,
        py_pred,
        search_r,
        &px_obs,
        &py_obs
      );
    }
    if (ok_c) {
      src[n][0] = mx;
      src[n][1] = my;
      dst[n][0] = px_obs;
      dst[n][1] = py_obs;
      n++;
    }
  }
  for (int row = 7; row <= qr_size - 8; row++) {
    if (n >= MAX_CORR) {
      break;
    }
    double mx = 6.5;
    double my = (double)row + 0.5;
    double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
    if (fabs(pw) < 1e-10) {
      continue;
    }
    double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
    double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
    double px_obs, py_obs;
    int ok_c;
    if ((row % 2) == 0) {
      ok_c = find_dark_centroid_local(
        gray,
        w,
        h,
        px_pred,
        py_pred,
        search_r,
        &px_obs,
        &py_obs
      );
    }
    else {
      ok_c = find_light_centroid_local(
        gray,
        w,
        h,
        px_pred,
        py_pred,
        search_r,
        &px_obs,
        &py_obs
      );
    }
    if (ok_c) {
      src[n][0] = mx;
      src[n][1] = my;
      dst[n][0] = px_obs;
      dst[n][1] = py_obs;
      n++;
    }
  }
  /* Alignment pattern anchors: sample the 8 known-light inner-ring cells
     around the BR alignment center to pin the quadratic warp's BR quadrant
     — without this the warp is only constrained by the single alignment
     center in that region and extrapolates wildly into the data area. */
  if (have_alignment) {
    double ax = (double)qr_size - 6.5; /* alignment center in module coords */
    double ay = (double)qr_size - 6.5;
    static const double ring1[8][2] = {
      {-1.0, -1.0},
      {0.0, -1.0},
      {1.0, -1.0},
      {-1.0, 0.0},
      {1.0, 0.0},
      {-1.0, 1.0},
      {0.0, 1.0},
      {1.0, 1.0},
    };
    for (int i = 0; i < 8; i++) {
      if (n >= MAX_CORR) {
        break;
      }
      double mx = ax + ring1[i][0];
      double my = ay + ring1[i][1];
      double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
      if (fabs(pw) < 1e-10) {
        continue;
      }
      double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
      double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
      double px_obs, py_obs;
      if (find_light_centroid_local(
            gray,
            w,
            h,
            px_pred,
            py_pred,
            search_r,
            &px_obs,
            &py_obs
          )) {
        src[n][0] = mx;
        src[n][1] = my;
        dst[n][0] = px_obs;
        dst[n][1] = py_obs;
        n++;
      }
    }
  }
  /* Need at least 8 well-spread observations for a stable quadratic fit
     (6 unknowns per axis + a bit of over-determination). */
  if (n < 8) {
    return NULL;
  }
  double coeffs[12];
  if (!fit_quadratic_warp(src, dst, n, coeffs)) {
    return NULL;
  }
  uint8_t *grid = sample_grid_quadratic(bin, w, h, coeffs, qr_size);
  if (!grid) {
    return NULL;
  }
  int fd = 16, rc = 100000;
  char *d = decode_grid(grid, NULL, qr_size, version, &fd, &rc);
  free(grid);
  if (d) {
    if (out_fmt_dist) {
      *out_fmt_dist = fd;
    }
    if (out_rs_cost) {
      *out_rs_cost = rc;
    }
  }
  return d;
}

/* Refine a homography by observing pixel-space centroids of every dark
   module in the two QR timing patterns and solving a least-squares fit
   over the combined set of control points and timing observations. The
   3 finder centers (and optional BR alignment) give the coarse shape;
   the timing observations, which thread the QR's interior, constrain
   the transform through the data area and correct the sub-module drift
   that a 4-point homography can't resolve on curled or lens-distorted
   captures (e.g. a receipt photographed at an angle). Returns 1 if the
   refined H is valid and used ≥ 6 points total. Leaves H_out untouched
   on failure. */
static int refine_h_with_timing(
  uint8_t const *gray,
  int w,
  int h,
  const double H_init[9],
  int qr_size,
  float ms,
  FinderPattern tl,
  FinderPattern tr,
  FinderPattern bl,
  int have_alignment,
  double alignment_x,
  double alignment_y,
  double H_out[9]
) {
  if (!gray || qr_size < 21) {
    return 0;
  }
  enum { MAX_CORR = 80 };
  double src[MAX_CORR][2];
  double dst[MAX_CORR][2];
  int n = 0;

  /* 3 finder centers as anchoring control points. */
  src[n][0] = 3.5;
  src[n][1] = 3.5;
  dst[n][0] = tl.x;
  dst[n][1] = tl.y;
  n++;
  src[n][0] = qr_size - 3.5;
  src[n][1] = 3.5;
  dst[n][0] = tr.x;
  dst[n][1] = tr.y;
  n++;
  src[n][0] = 3.5;
  src[n][1] = qr_size - 3.5;
  dst[n][0] = bl.x;
  dst[n][1] = bl.y;
  n++;

  /* Optional alignment-pattern center for the BR corner. */
  if (have_alignment) {
    src[n][0] = qr_size - 6.5;
    src[n][1] = qr_size - 6.5;
    dst[n][0] = alignment_x;
    dst[n][1] = alignment_y;
    n++;
  }

  /* Timing pattern: module row 6 columns 8..qr_size-9, dark at even cols;
     module col 6 rows 8..qr_size-9, dark at even rows. Sample the dark
     modules' pixel-space centroids using the initial H as a guide and a
     half-module search radius — the initial guess is within a module's
     width of truth on realistic captures. */
  float search_r = ms * 0.5f;
  for (int col = 8; col <= qr_size - 9; col += 2) {
    if (n >= MAX_CORR) {
      break;
    }
    double mx = (double)col + 0.5;
    double my = 6.5;
    double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
    if (fabs(pw) < 1e-10) {
      continue;
    }
    double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
    double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
    double px_obs, py_obs;
    if (find_dark_centroid_local(
          gray,
          w,
          h,
          px_pred,
          py_pred,
          search_r,
          &px_obs,
          &py_obs
        )) {
      src[n][0] = mx;
      src[n][1] = my;
      dst[n][0] = px_obs;
      dst[n][1] = py_obs;
      n++;
    }
  }
  for (int row = 8; row <= qr_size - 9; row += 2) {
    if (n >= MAX_CORR) {
      break;
    }
    double mx = 6.5;
    double my = (double)row + 0.5;
    double pw = H_init[6] * mx + H_init[7] * my + H_init[8];
    if (fabs(pw) < 1e-10) {
      continue;
    }
    double px_pred = (H_init[0] * mx + H_init[1] * my + H_init[2]) / pw;
    double py_pred = (H_init[3] * mx + H_init[4] * my + H_init[5]) / pw;
    double px_obs, py_obs;
    if (find_dark_centroid_local(
          gray,
          w,
          h,
          px_pred,
          py_pred,
          search_r,
          &px_obs,
          &py_obs
        )) {
      src[n][0] = mx;
      src[n][1] = my;
      dst[n][0] = px_obs;
      dst[n][1] = py_obs;
      n++;
    }
  }

  /* Need meaningfully more than the 4-point minimum for the least-squares
     to actually do anything; if only the control points were observed
     (all timing centroid calls failed — likely a low-contrast or obscured
     QR) the refined H would equal the initial one. */
  if (n < 6) {
    return 0;
  }
  return compute_homography_ls(src, dst, n, H_out);
}

/* ---- Alignment pattern search ---- */

/* Check if (cx, cy) looks like the center of a QR alignment pattern:
   dark center with light ring 1 module out and dark ring 2 modules out.
   Samples along 8 radial directions at 1 and 2 module offsets, and
   requires at least 6 of 8 each to match. */
static int
is_alignment_center(uint8_t const *px, int w, int h, int cx, int cy, float ms) {
  if (cx < 0 || cx >= w || cy < 0 || cy >= h) {
    return 0;
  }
  if (!is_dark(px, w, h, cx, cy)) {
    return 0;
  }
  /* 8 radial directions */
  static const float dirs[8][2] = {
    {1.0f, 0.0f},
    {0.707f, 0.707f},
    {0.0f, 1.0f},
    {-0.707f, 0.707f},
    {-1.0f, 0.0f},
    {-0.707f, -0.707f},
    {0.0f, -1.0f},
    {0.707f, -0.707f},
  };
  int light_ok = 0;
  int dark_ring_ok = 0;
  int beyond_light_ok = 0;
  int beyond_checked = 0;
  for (int d = 0; d < 8; d++) {
    int lx = (int)(cx + ms * dirs[d][0] + 0.5f * (dirs[d][0] > 0 ? 1 : -1));
    int ly = (int)(cy + ms * dirs[d][1] + 0.5f * (dirs[d][1] > 0 ? 1 : -1));
    int rx =
      (int)(cx + 2.0f * ms * dirs[d][0] + 0.5f * (dirs[d][0] > 0 ? 1 : -1));
    int ry =
      (int)(cy + 2.0f * ms * dirs[d][1] + 0.5f * (dirs[d][1] > 0 ? 1 : -1));
    /* A real alignment pattern is exactly 5 modules wide: the outer dark
       ring sits at radius 2·ms, so just beyond (2.8·ms) we must be out of
       the pattern again. Data-module clusters that happen to expose a
       dark center + light 1·ms + dark 2·ms often keep going dark past
       radius 2, because they're embedded in a larger dense region —
       require "light past the dark ring" in the majority of directions
       to rule those out. */
    int bx =
      (int)(cx + 2.8f * ms * dirs[d][0] + 0.5f * (dirs[d][0] > 0 ? 1 : -1));
    int by =
      (int)(cy + 2.8f * ms * dirs[d][1] + 0.5f * (dirs[d][1] > 0 ? 1 : -1));
    if (lx < 0 || lx >= w || ly < 0 || ly >= h) {
      continue;
    }
    if (rx < 0 || rx >= w || ry < 0 || ry >= h) {
      continue;
    }
    if (!is_dark(px, w, h, lx, ly)) {
      light_ok++;
    }
    if (is_dark(px, w, h, rx, ry)) {
      dark_ring_ok++;
    }
    if (bx >= 0 && bx < w && by >= 0 && by < h) {
      beyond_checked++;
      if (!is_dark(px, w, h, bx, by)) {
        beyond_light_ok++;
      }
    }
  }
  if (light_ok < 6 || dark_ring_ok < 6) {
    return 0;
  }
  /* Require ≥ 5 of 8 outer directions to be light beyond the dark ring.
     5 (of 8) keeps a small margin for alignment patterns adjacent to a
     single dark data module while still killing patterns embedded in
     dark clusters. If we couldn't check enough directions (pattern near
     image edge), pass through — the existing 2-ring check has to carry. */
  if (beyond_checked >= 5 && beyond_light_ok < 5) {
    return 0;
  }
  return 1;
}

static int find_alignment_pattern(
  uint8_t const *px,
  int w,
  int h,
  float cx_pred,
  float cy_pred,
  float expected_ms,
  float search_radius,
  float *out_cx,
  float *out_cy
) {
  int x_min = (int)(cx_pred - search_radius);
  int x_max = (int)(cx_pred + search_radius);
  int y_min = (int)(cy_pred - search_radius);
  int y_max = (int)(cy_pred + search_radius);
  if (x_min < 0) {
    x_min = 0;
  }
  if (y_min < 0) {
    y_min = 0;
  }
  if (x_max >= w) {
    x_max = w - 1;
  }
  if (y_max >= h) {
    y_max = h - 1;
  }
  if (x_max <= x_min || y_max <= y_min) {
    return 0;
  }
  /* Scan every dark pixel; the one with the smallest distance to the
     predicted center that also passes the template check wins. */
  float best_cx = -1, best_cy = -1;
  float best_dist = 1e30f;
  for (int y = y_min; y <= y_max; y++) {
    for (int x = x_min; x <= x_max; x++) {
      if (!is_dark(px, w, h, x, y)) {
        continue;
      }
      if (!is_alignment_center(px, w, h, x, y, expected_ms)) {
        continue;
      }
      float dx = (float)x - cx_pred;
      float dy = (float)y - cy_pred;
      float dist = dx * dx + dy * dy;
      if (dist < best_dist) {
        best_dist = dist;
        best_cx = (float)x;
        best_cy = (float)y;
      }
    }
  }
  if (best_cx < 0) {
    return 0;
  }
  /* Refine by averaging all near-center dark pixels (the center module is
     ~1 module wide, so there will be a cluster of passing pixels). */
  int r_refine = (int)(expected_ms * 0.5f);
  if (r_refine < 1) {
    r_refine = 1;
  }
  double sum_x = 0, sum_y = 0;
  int count = 0;
  for (int dy = -r_refine; dy <= r_refine; dy++) {
    for (int dx = -r_refine; dx <= r_refine; dx++) {
      int x = (int)best_cx + dx;
      int y = (int)best_cy + dy;
      if (x < 0 || x >= w || y < 0 || y >= h) {
        continue;
      }
      if (is_dark(px, w, h, x, y) &&
          is_alignment_center(px, w, h, x, y, expected_ms)) {
        sum_x += x;
        sum_y += y;
        count++;
      }
    }
  }
  if (count > 0) {
    *out_cx = (float)(sum_x / count);
    *out_cy = (float)(sum_y / count);
  }
  else {
    *out_cx = best_cx;
    *out_cy = best_cy;
  }
  return 1;
}

/* ---- Alignment pattern grid positions (ISO/IEC 18004 Annex E) ----
   Each row lists the grid coordinates (in modules, 0-indexed) where
   alignment pattern centres lie for that version. Alignment patterns
   exist at every combination (row, col) of these values EXCEPT the
   three that overlap the finder patterns: (first, first), (first, last),
   (last, first). Index = version. Count is stored in [0]; up to 7 values
   follow in [1..7]. v1 has no alignment patterns. */
static const int QR_ALIGN_GRID[41][8] = {
  {0}, /* v0 unused */
  {0}, /* v1 */
  {2, 6, 18},
  {2, 6, 22},
  {2, 6, 26},
  {2, 6, 30},
  {2, 6, 34},
  {3, 6, 22, 38},
  {3, 6, 24, 42},
  {3, 6, 26, 46},
  {3, 6, 28, 50},
  {3, 6, 30, 54},
  {3, 6, 32, 58},
  {3, 6, 34, 62},
  {4, 6, 26, 46, 66},
  {4, 6, 26, 48, 70},
  {4, 6, 26, 50, 74},
  {4, 6, 30, 54, 78},
  {4, 6, 30, 56, 82},
  {4, 6, 30, 58, 86},
  {4, 6, 34, 62, 90},
  {5, 6, 28, 50, 72, 94},
  {5, 6, 26, 50, 74, 98},
  {5, 6, 30, 54, 78, 102},
  {5, 6, 28, 54, 80, 106},
  {5, 6, 32, 58, 84, 110},
  {5, 6, 30, 58, 86, 114},
  {5, 6, 34, 62, 90, 118},
  {6, 6, 26, 50, 74, 98, 122},
  {6, 6, 30, 54, 78, 102, 126},
  {6, 6, 26, 52, 78, 104, 130},
  {6, 6, 30, 56, 82, 108, 134},
  {6, 6, 34, 60, 86, 112, 138},
  {6, 6, 30, 58, 86, 114, 142},
  {6, 6, 34, 62, 90, 118, 146},
  {7, 6, 30, 54, 78, 102, 126, 150},
  {7, 6, 24, 50, 76, 102, 128, 154},
  {7, 6, 28, 54, 80, 106, 132, 158},
  {7, 6, 32, 58, 84, 110, 136, 162},
  {7, 6, 26, 54, 82, 110, 138, 166},
  {7, 6, 30, 58, 86, 114, 142, 170},
};

/* Apply a 3x3 homography H (row-major) to module coordinates (mx, my),
   returning pixel coordinates. Shared with the quadratic-warp path. */
static int project_homography(
  const double H[9],
  double mx,
  double my,
  double *out_px,
  double *out_py
) {
  double w_h = H[6] * mx + H[7] * my + H[8];
  if (fabs(w_h) < 1e-10) {
    return 0;
  }
  *out_px = (H[0] * mx + H[1] * my + H[2]) / w_h;
  *out_py = (H[3] * mx + H[4] * my + H[5]) / w_h;
  return 1;
}

/* Find every alignment pattern predicted by H for this version, skipping
   the three that overlap the finders. Writes (module-coord, pixel-coord)
   pairs for each one found, and returns the count. `search_r_modules`
   controls how far find_alignment_pattern looks from each predicted
   centre — widen it when H is an unrefined affine extrapolation from
   three finders (the residual under strong perspective can be 3-4
   modules), tighten it when H already folds in the BR alignment. */
static int find_all_alignment_patterns(
  uint8_t const *px,
  int w,
  int h,
  int version,
  const double H[9],
  float avg_m,
  float search_r_modules,
  double out_src[][2],
  double out_dst[][2],
  int max_out
) {
  if (version < 2 || version > 40) {
    return 0;
  }
  int nvals = QR_ALIGN_GRID[version][0];
  if (nvals < 2) {
    return 0;
  }
  int last = QR_ALIGN_GRID[version][nvals];
  int first = QR_ALIGN_GRID[version][1];
  float search_r = avg_m * search_r_modules;
  int found = 0;
  for (int ri = 0; ri < nvals; ri++) {
    int row = QR_ALIGN_GRID[version][1 + ri];
    for (int ci = 0; ci < nvals; ci++) {
      int col = QR_ALIGN_GRID[version][1 + ci];
      /* Skip the three finder-overlapping positions. */
      if ((row == first && col == first) || (row == first && col == last) ||
          (row == last && col == first)) {
        continue;
      }
      if (found >= max_out) {
        return found;
      }
      double mx = (double)col + 0.5;
      double my = (double)row + 0.5;
      double pred_x, pred_y;
      if (!project_homography(H, mx, my, &pred_x, &pred_y)) {
        continue;
      }
      float found_x, found_y;
      if (find_alignment_pattern(
            px,
            w,
            h,
            (float)pred_x,
            (float)pred_y,
            avg_m,
            search_r,
            &found_x,
            &found_y
          )) {
        out_src[found][0] = mx;
        out_src[found][1] = my;
        out_dst[found][0] = found_x;
        out_dst[found][1] = found_y;
        found++;
      }
    }
  }
  return found;
}

/* ---- Grid sampling ----
   Homography-only: uses a 4-point perspective transform and samples a small
   cluster of pixels near each module center, taking the majority value to
   tolerate minor grid misalignment. The affine-equivalent case (no
   perspective) is expressible as a homography where the 4th point is the
   parallelogram completion, so a separate affine path isn't needed. */
/* Bilinear sample of the binary image at fractional pixel (fx, fy). Returns
   the 0..255 weighted blend of the four neighbouring pixels' dark-ness. */
static int bilinear_dark(uint8_t const *px, int w, int h, float fx, float fy) {
  int x0 = (int)floorf(fx);
  int y0 = (int)floorf(fy);
  int x1 = x0 + 1;
  int y1 = y0 + 1;
  float dx = fx - x0;
  float dy = fy - y0;
  if (x0 < 0) {
    x0 = 0;
  }
  if (y0 < 0) {
    y0 = 0;
  }
  if (x1 < 0) {
    x1 = 0;
  }
  if (y1 < 0) {
    y1 = 0;
  }
  if (x0 >= w) {
    x0 = w - 1;
  }
  if (y0 >= h) {
    y0 = h - 1;
  }
  if (x1 >= w) {
    x1 = w - 1;
  }
  if (y1 >= h) {
    y1 = h - 1;
  }
  int v00 = px[y0 * w + x0] < 128 ? 1 : 0;
  int v01 = px[y0 * w + x1] < 128 ? 1 : 0;
  int v10 = px[y1 * w + x0] < 128 ? 1 : 0;
  int v11 = px[y1 * w + x1] < 128 ? 1 : 0;
  float v0 = v00 * (1.0f - dx) + v01 * dx;
  float v1 = v10 * (1.0f - dx) + v11 * dx;
  return (v0 * (1.0f - dy) + v1 * dy) >= 0.5f;
}

/* Sample a single grayscale value via bilinear interpolation, with edge
   clamping. Returns 0 if input gray is NULL (signals "no gray available"). */
static int
sample_gray_bilinear(uint8_t const *gray, int w, int h, float fx, float fy) {
  if (!gray) {
    return -1;
  }
  int x0 = (int)floorf(fx);
  int y0 = (int)floorf(fy);
  int x1 = x0 + 1;
  int y1 = y0 + 1;
  float dx = fx - x0;
  float dy = fy - y0;
  if (x0 < 0) {
    x0 = 0;
  }
  if (y0 < 0) {
    y0 = 0;
  }
  if (x1 < 0) {
    x1 = 0;
  }
  if (y1 < 0) {
    y1 = 0;
  }
  if (x0 >= w) {
    x0 = w - 1;
  }
  if (y0 >= h) {
    y0 = h - 1;
  }
  if (x1 >= w) {
    x1 = w - 1;
  }
  if (y1 >= h) {
    y1 = h - 1;
  }
  float v00 = gray[y0 * w + x0];
  float v01 = gray[y0 * w + x1];
  float v10 = gray[y1 * w + x0];
  float v11 = gray[y1 * w + x1];
  float v0 = v00 * (1.0f - dx) + v01 * dx;
  float v1 = v10 * (1.0f - dx) + v11 * dx;
  float v = v0 * (1.0f - dy) + v1 * dy;
  if (v < 0.0f) {
    v = 0.0f;
  }
  if (v > 255.0f) {
    v = 255.0f;
  }
  return (int)(v + 0.5f);
}

/* Sample the QR module grid given a homography. If `gray_for_conf` is
   non-NULL and `out_conf` is non-NULL, also writes a per-module ambiguity
   buffer (1 = low-confidence module, 0 = high-confidence) of the same size
   as the grid. The caller is responsible for free()ing both buffers.

   Confidence is derived adaptively: after sampling all modules and their
   per-module mean grayscale, compute the median grayscale of the modules
   classified as light vs dark. A module is flagged ambiguous when its
   mean gray sits a long way from BOTH expected values (signals an
   occlusion patch whose color belongs to neither QR cluster) or when the
   5 sub-sample votes were a near-tie (signals the patch boundary crosses
   the module's sample area). */
static uint8_t *sample_grid_homography_offset_mode(
  uint8_t const *px,
  uint8_t const *gray_for_conf,
  int w,
  int h,
  const double H[9],
  int qr_size,
  double off_x,
  double off_y,
  int use_bilinear,
  uint8_t **out_conf
) {
  int total = qr_size * qr_size;
  uint8_t *grid = calloc(total, 1);
  if (!grid) {
    return NULL;
  }
  uint8_t *conf = NULL;
  uint8_t *gmean_buf = NULL;
  uint8_t *vote_tie = NULL;
  if (out_conf && gray_for_conf) {
    conf = calloc(total, 1);
    gmean_buf = malloc(total);
    vote_tie = calloc(total, 1);
    if (!conf || !gmean_buf || !vote_tie) {
      free(grid);
      free(conf);
      free(gmean_buf);
      free(vote_tie);
      return NULL;
    }
  }
  static const double offs[5][2] = {
    {0.5, 0.5},
    {0.35, 0.35},
    {0.65, 0.35},
    {0.35, 0.65},
    {0.65, 0.65},
  };
  for (int r = 0; r < qr_size; r++) {
    for (int c = 0; c < qr_size; c++) {
      int votes = 0;
      int gray_sum = 0;
      int gray_n = 0;
      for (int o = 0; o < 5; o++) {
        float fx, fy;
        apply_homography(
          H,
          c + offs[o][0] + off_x,
          r + offs[o][1] + off_y,
          &fx,
          &fy
        );
        int dark;
        if (use_bilinear) {
          dark = bilinear_dark(px, w, h, fx, fy);
        }
        else {
          int ix = (int)(fx + 0.5f);
          int iy = (int)(fy + 0.5f);
          dark = is_dark(px, w, h, ix, iy);
        }
        if (dark) {
          votes++;
        }
        if (gmean_buf) {
          int g = sample_gray_bilinear(gray_for_conf, w, h, fx, fy);
          if (g >= 0) {
            gray_sum += g;
            gray_n++;
          }
        }
      }
      grid[r * qr_size + c] = (votes >= 3) ? 1 : 0;
      if (gmean_buf) {
        int gmean = (gray_n > 0) ? (gray_sum / gray_n) : 128;
        if (gmean < 0) {
          gmean = 0;
        }
        if (gmean > 255) {
          gmean = 255;
        }
        gmean_buf[r * qr_size + c] = (uint8_t)gmean;
        if (vote_tie) {
          vote_tie[r * qr_size + c] =
            (uint8_t)((votes == 2 || votes == 3) ? 1 : 0);
        }
      }
    }
  }

  if (conf && gmean_buf) {
    /* Compute medians of dark / light cluster grays via histogram. */
    int hist_dark[256] = {0};
    int hist_light[256] = {0};
    int n_dark = 0, n_light = 0;
    for (int i = 0; i < total; i++) {
      if (grid[i]) {
        hist_dark[gmean_buf[i]]++;
        n_dark++;
      }
      else {
        hist_light[gmean_buf[i]]++;
        n_light++;
      }
    }
    int dark_med = 0, light_med = 255;
    if (n_dark > 0) {
      int target = n_dark / 2;
      int acc = 0;
      for (int v = 0; v < 256; v++) {
        acc += hist_dark[v];
        if (acc > target) {
          dark_med = v;
          break;
        }
      }
    }
    if (n_light > 0) {
      int target = n_light / 2;
      int acc = 0;
      for (int v = 0; v < 256; v++) {
        acc += hist_light[v];
        if (acc > target) {
          light_med = v;
          break;
        }
      }
    }
    /* Tolerance: a chunky fraction of the cluster gap. A module is flagged
       only when its mean gray is FAR from BOTH the dark and the light
       cluster medians — that is the strong signal that the pixel belongs
       to a foreign region (occlusion patch). We also keep a floor so the
       rule still bites when the clusters are weakly separated. */
    int gap = light_med - dark_med;
    if (gap < 0) {
      gap = -gap;
    }
    int tol = gap / 3;
    if (tol < 35) {
      tol = 35;
    }
    if (tol > 80) {
      tol = 80;
    }
    for (int i = 0; i < total; i++) {
      int g = gmean_buf[i];
      int dd = g - dark_med;
      if (dd < 0) {
        dd = -dd;
      }
      int dl = g - light_med;
      if (dl < 0) {
        dl = -dl;
      }
      int ambig = 0;
      if (dd > tol && dl > tol) {
        ambig = 1;
      }
      conf[i] = (uint8_t)ambig;
    }
  }

  free(gmean_buf);
  free(vote_tie);
  if (out_conf) {
    *out_conf = conf;
  }
  return grid;
}

/* ---- Timing pattern check ----
   The timing patterns on row 6 and column 6 alternate dark/light between the
   three finder patterns. Starting at module (6, 8) = dark, (6, 9) = light,
   (6, 10) = dark, … — dark whenever col or row is even. Real QRs have a
   near-perfect match; a grid sampled from three incidental 1:1:3:1:1 runs in
   noise produces essentially random bits here (~50% error). Returns the
   fraction of timing modules whose value disagrees with the spec, 0.0 for a
   perfect match and up to 1.0 for all wrong. */
static float timing_error_rate(uint8_t const *grid, int qr_size) {
  if (qr_size < 15) {
    return 0.0f;
  }
  int errors = 0;
  int total = 0;
  for (int c = 8; c <= qr_size - 9; c++) {
    int expected = ((c % 2) == 0) ? 1 : 0;
    int actual = grid[6 * qr_size + c] ? 1 : 0;
    if (actual != expected) {
      errors++;
    }
    total++;
  }
  for (int r = 8; r <= qr_size - 9; r++) {
    int expected = ((r % 2) == 0) ? 1 : 0;
    int actual = grid[r * qr_size + 6] ? 1 : 0;
    if (actual != expected) {
      errors++;
    }
    total++;
  }
  if (total == 0) {
    return 1.0f;
  }
  return (float)errors / (float)total;
}

/* ---- Finder pattern structural check ----
   After sampling, each of the three 7x7 finder regions should match the
   canonical dark/light pattern (outer ring dark, 1-module light inset, 3x3
   dark core). Plus the 1-module separator (row/col between finder and data)
   should be light. Incidental 1:1:3:1:1 runs in noise almost never project
   into grids whose TL/TR/BL 7x7 windows all match the finder pattern.
   Returns the fraction of the 48 checked cells (16 per finder) that
   disagree — callers reject above ~0.10. */
static const uint8_t FINDER_PATTERN[7][7] = {
  {1, 1, 1, 1, 1, 1, 1},
  {1, 0, 0, 0, 0, 0, 1},
  {1, 0, 1, 1, 1, 0, 1},
  {1, 0, 1, 1, 1, 0, 1},
  {1, 0, 1, 1, 1, 0, 1},
  {1, 0, 0, 0, 0, 0, 1},
  {1, 1, 1, 1, 1, 1, 1},
};

static float finder_error_rate(uint8_t const *grid, int qr_size) {
  if (qr_size < 21) {
    return 1.0f;
  }
  int errors = 0;
  int total = 0;
  /* TL at (0,0); TR at (0, qr_size-7); BL at (qr_size-7, 0). */
  const int origins[3][2] = {
    {0, 0},
    {0, qr_size - 7},
    {qr_size - 7, 0},
  };
  for (int f = 0; f < 3; f++) {
    int r0 = origins[f][0];
    int c0 = origins[f][1];
    for (int dr = 0; dr < 7; dr++) {
      for (int dc = 0; dc < 7; dc++) {
        int expected = FINDER_PATTERN[dr][dc];
        int actual = grid[(r0 + dr) * qr_size + (c0 + dc)] ? 1 : 0;
        if (actual != expected) {
          errors++;
        }
        total++;
      }
    }
  }
  if (total == 0) {
    return 1.0f;
  }
  return (float)errors / (float)total;
}

/* ---- Format information ---- */

static const int FMT_COORDS1[15][2] = {
  {8, 0},
  {8, 1},
  {8, 2},
  {8, 3},
  {8, 4},
  {8, 5},
  {8, 7},
  {8, 8},
  {7, 8},
  {5, 8},
  {4, 8},
  {3, 8},
  {2, 8},
  {1, 8},
  {0, 8},
};

static const uint32_t VALID_FMTS[32] = {
  0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0,
  0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976,
  0x1689, 0x13BE, 0x1CE7, 0x19D0, 0x0762, 0x0255, 0x0D0C, 0x083B,
  0x355F, 0x3068, 0x3F31, 0x3A06, 0x24B4, 0x2183, 0x2EDA, 0x2BED,
};

static int popcount_u(uint32_t v) {
  int c = 0;
  while (v) {
    c += v & 1;
    v >>= 1;
  }
  return c;
}

static int decode_format(
  uint8_t const *grid,
  int qr_size,
  int *ec_level,
  int *mask_pattern,
  int *out_dist
) {
  uint32_t bits1 = 0;
  for (int i = 0; i < 15; i++) {
    int col = FMT_COORDS1[i][0], row = FMT_COORDS1[i][1];
    if (row < qr_size && col < qr_size && grid[row * qr_size + col]) {
      bits1 |= (1u << i);
    }
  }
  uint32_t bits2 = 0;
  for (int i = 0; i < 7; i++) {
    int row = qr_size - 1 - i;
    if (grid[row * qr_size + 8]) {
      bits2 |= (1u << i);
    }
  }
  for (int i = 0; i < 8; i++) {
    int col = qr_size - 8 + i;
    if (grid[8 * qr_size + col]) {
      bits2 |= (1u << (7 + i));
    }
  }

  int best_d = 16, best_f = -1;
  for (int copy = 0; copy < 2; copy++) {
    uint32_t bits = (copy == 0) ? bits1 : bits2;
    for (int f = 0; f < 32; f++) {
      int d = popcount_u(bits ^ VALID_FMTS[f]);
      if (d < best_d) {
        best_d = d;
        best_f = f;
      }
    }
  }
  if (best_f < 0 || best_d > 3) {
    return -1;
  }

  int ec_ind = best_f >> 3;
  *mask_pattern = best_f & 7;
  static const int ec_map[4] = {1, 0, 3, 2};
  *ec_level = ec_map[ec_ind];
  if (out_dist) {
    *out_dist = best_d;
  }
  return 0;
}

/* ---- Masking ---- */

static int apply_mask(int mask, int row, int col) {
  switch (mask) {
  case 0:
    return (row + col) % 2 == 0;
  case 1:
    return row % 2 == 0;
  case 2:
    return col % 3 == 0;
  case 3:
    return (row + col) % 3 == 0;
  case 4:
    return (row / 2 + col / 3) % 2 == 0;
  case 5:
    return (row * col) % 2 + (row * col) % 3 == 0;
  case 6:
    return ((row * col) % 2 + (row * col) % 3) % 2 == 0;
  case 7:
    return ((row + col) % 2 + (row * col) % 3) % 2 == 0;
  default:
    return 0;
  }
}

/* ---- Function pattern check ---- */

static const int ALIGN_POS[41][8] = {
  {0},
  {0},
  {6, 18, 0},
  {6, 22, 0},
  {6, 26, 0},
  {6, 30, 0},
  {6, 34, 0},
  {6, 22, 38, 0},
  {6, 24, 42, 0},
  {6, 26, 46, 0},
  {6, 28, 50, 0},
  {6, 30, 54, 0},
  {6, 32, 58, 0},
  {6, 34, 62, 0},
  {6, 26, 46, 66, 0},
  {6, 26, 48, 70, 0},
  {6, 26, 50, 74, 0},
  {6, 30, 54, 78, 0},
  {6, 30, 56, 82, 0},
  {6, 30, 58, 86, 0},
  {6, 34, 62, 90, 0},
  {6, 28, 50, 72, 94, 0},
  {6, 26, 50, 74, 98, 0},
  {6, 30, 54, 78, 102, 0},
  {6, 28, 54, 80, 106, 0},
  {6, 32, 58, 84, 110, 0},
  {6, 30, 58, 86, 114, 0},
  {6, 34, 62, 90, 118, 0},
  {6, 26, 50, 74, 98, 122, 0},
  {6, 30, 54, 78, 102, 126, 0},
  {6, 26, 52, 78, 104, 130, 0},
  {6, 30, 56, 82, 108, 134, 0},
  {6, 34, 60, 86, 112, 138, 0},
  {6, 30, 58, 86, 114, 142, 0},
  {6, 34, 62, 90, 118, 146, 0},
  {6, 30, 54, 78, 102, 126, 150, 0},
  {6, 24, 50, 76, 102, 128, 154, 0},
  {6, 28, 54, 80, 106, 132, 158, 0},
  {6, 32, 58, 84, 110, 136, 162, 0},
  {6, 26, 54, 82, 110, 138, 166, 0},
  {6, 30, 58, 86, 114, 142, 170, 0},
};

static int is_function(int row, int col, int sz, int ver) {
  if (row <= 8 && col <= 8) {
    return 1;
  }
  if (row <= 8 && col >= sz - 8) {
    return 1;
  }
  if (row >= sz - 8 && col <= 8) {
    return 1;
  }
  if (row == 6 || col == 6) {
    return 1;
  }
  if (ver >= 7) {
    if (col >= sz - 11 && col <= sz - 9 && row <= 5) {
      return 1;
    }
    if (row >= sz - 11 && row <= sz - 9 && col <= 5) {
      return 1;
    }
  }
  if (ver >= 2 && ver <= 40) {
    const int *pos = ALIGN_POS[ver];
    int np = 0;
    while (pos[np] != 0 && np < 7) {
      np++;
    }
    for (int i = 0; i < np; i++) {
      for (int j = 0; j < np; j++) {
        int ay = pos[i], ax = pos[j];
        if (ay <= 8 && ax <= 8) {
          continue;
        }
        if (ay <= 8 && ax >= sz - 8) {
          continue;
        }
        if (ay >= sz - 8 && ax <= 8) {
          continue;
        }
        if (abs(row - ay) <= 2 && abs(col - ax) <= 2) {
          return 1;
        }
      }
    }
  }
  return 0;
}

/* ---- Data extraction in zigzag order ---- */

static int extract_bits(
  uint8_t const *grid,
  uint8_t const *conf,
  int sz,
  int ver,
  int mask,
  uint8_t *bits,
  uint8_t *bit_conf,
  int max_bits
) {
  int n = 0, up = 1;
  for (int right = sz - 1; right >= 1; right -= 2) {
    if (right == 6) {
      right = 5;
    }
    for (int i = 0; i < sz; i++) {
      int row = up ? (sz - 1 - i) : i;
      for (int c = 0; c < 2; c++) {
        int col = right - c;
        if (col < 0) {
          continue;
        }
        if (!is_function(row, col, sz, ver) && n < max_bits) {
          int val = grid[row * sz + col];
          if (apply_mask(mask, row, col)) {
            val ^= 1;
          }
          bits[n] = (uint8_t)val;
          if (bit_conf) {
            bit_conf[n] = conf ? conf[row * sz + col] : 0;
          }
          n++;
        }
      }
    }
    up = !up;
  }
  return n;
}

/* ---- Bits to bytes ---- */

static int bits_to_bytes(
  uint8_t const *bits,
  uint8_t const *bit_conf,
  int nbits,
  uint8_t *bytes,
  uint8_t *byte_ambig
) {
  int nb = nbits / 8;
  for (int i = 0; i < nb; i++) {
    uint8_t b = 0;
    int ambig = 0;
    for (int j = 0; j < 8; j++) {
      b = (b << 1) | bits[i * 8 + j];
      if (bit_conf && bit_conf[i * 8 + j]) {
        ambig = 1;
      }
    }
    bytes[i] = b;
    if (byte_ambig) {
      byte_ambig[i] = (uint8_t)ambig;
    }
  }
  return nb;
}

/* ---- Read N bits from byte array ---- */

static int read_bits(uint8_t const *data, int len, int *pos, int n) {
  int val = 0;
  for (int i = 0; i < n; i++) {
    int bi = *pos + i;
    int byte_idx = bi / 8, bit_idx = 7 - (bi % 8);
    if (byte_idx < len) {
      val = (val << 1) | ((data[byte_idx] >> bit_idx) & 1);
    }
    else {
      val <<= 1;
    }
  }
  *pos += n;
  return val;
}

/* ---- EC table ---- */

typedef struct {
  int total_cw, ec_cw_per_block;
  int g1_blocks, g1_data_cw;
  int g2_blocks, g2_data_cw;
} ECInfo;

static const ECInfo EC_TABLE[40][4] = {
  /* L                            M                         Q H */
  /* v1  */ {
    {26, 7, 1, 19, 0, 0},
    {26, 10, 1, 16, 0, 0},
    {26, 13, 1, 13, 0, 0},
    {26, 17, 1, 9, 0, 0}
  },
  /* v2  */
  {{44, 10, 1, 34, 0, 0},
   {44, 16, 1, 28, 0, 0},
   {44, 22, 1, 22, 0, 0},
   {44, 28, 1, 16, 0, 0}},
  /* v3  */
  {{70, 15, 1, 55, 0, 0},
   {70, 26, 1, 44, 0, 0},
   {70, 18, 2, 17, 0, 0},
   {70, 22, 2, 13, 0, 0}},
  /* v4  */
  {{100, 20, 1, 80, 0, 0},
   {100, 18, 2, 32, 0, 0},
   {100, 26, 2, 24, 0, 0},
   {100, 16, 4, 9, 0, 0}},
  /* v5  */
  {{134, 26, 1, 108, 0, 0},
   {134, 24, 2, 43, 0, 0},
   {134, 18, 2, 15, 2, 16},
   {134, 22, 2, 11, 2, 12}},
  /* v6  */
  {{172, 18, 2, 68, 0, 0},
   {172, 16, 4, 27, 0, 0},
   {172, 24, 4, 19, 0, 0},
   {172, 28, 4, 15, 0, 0}},
  /* v7  */
  {{196, 20, 2, 78, 0, 0},
   {196, 18, 4, 31, 0, 0},
   {196, 18, 2, 14, 4, 15},
   {196, 26, 4, 13, 1, 14}},
  /* v8  */
  {{242, 24, 2, 97, 0, 0},
   {242, 22, 2, 38, 2, 39},
   {242, 22, 4, 18, 2, 19},
   {242, 26, 4, 14, 2, 15}},
  /* v9  */
  {{292, 30, 2, 116, 0, 0},
   {292, 22, 3, 36, 2, 37},
   {292, 20, 4, 16, 4, 17},
   {292, 24, 4, 12, 4, 13}},
  /* v10 */
  {{346, 18, 2, 68, 2, 69},
   {346, 26, 4, 43, 1, 44},
   {346, 24, 6, 19, 2, 20},
   {346, 28, 6, 15, 2, 16}},
  /* v11 */
  {{404, 20, 4, 81, 0, 0},
   {404, 30, 1, 50, 4, 51},
   {404, 28, 4, 22, 4, 23},
   {404, 24, 3, 12, 8, 13}},
  /* v12 */
  {{466, 24, 2, 92, 2, 93},
   {466, 22, 6, 36, 2, 37},
   {466, 26, 4, 20, 6, 21},
   {466, 28, 7, 14, 4, 15}},
  /* v13 */
  {{532, 26, 4, 107, 0, 0},
   {532, 22, 8, 37, 1, 38},
   {532, 24, 8, 20, 4, 21},
   {532, 22, 12, 11, 4, 12}},
  /* v14 */
  {{581, 30, 3, 115, 1, 116},
   {581, 24, 4, 40, 5, 41},
   {581, 20, 11, 16, 5, 17},
   {581, 24, 11, 12, 5, 13}},
  /* v15 */
  {{655, 22, 5, 87, 1, 88},
   {655, 24, 5, 41, 5, 42},
   {655, 30, 5, 24, 7, 25},
   {655, 24, 11, 12, 7, 13}},
  /* v16 */
  {{733, 24, 5, 98, 1, 99},
   {733, 28, 7, 45, 3, 46},
   {733, 24, 15, 19, 2, 20},
   {733, 30, 3, 15, 13, 16}},
  /* v17 */
  {{815, 28, 1, 107, 5, 108},
   {815, 28, 10, 46, 1, 47},
   {815, 28, 1, 22, 15, 23},
   {815, 28, 2, 14, 17, 15}},
  /* v18 */
  {{901, 30, 5, 120, 1, 121},
   {901, 26, 9, 43, 4, 44},
   {901, 28, 17, 22, 1, 23},
   {901, 28, 2, 14, 19, 15}},
  /* v19 */
  {{991, 28, 3, 113, 4, 114},
   {991, 26, 3, 44, 11, 45},
   {991, 26, 17, 21, 4, 22},
   {991, 26, 9, 13, 16, 14}},
  /* v20 */
  {{1085, 28, 3, 107, 5, 108},
   {1085, 26, 3, 41, 13, 42},
   {1085, 30, 15, 24, 5, 25},
   {1085, 28, 15, 15, 10, 16}},
  /* v21 */
  {{1156, 28, 4, 116, 4, 117},
   {1156, 26, 17, 42, 0, 0},
   {1156, 28, 17, 22, 6, 23},
   {1156, 30, 19, 16, 6, 17}},
  /* v22 */
  {{1258, 28, 2, 111, 7, 112},
   {1258, 28, 17, 46, 0, 0},
   {1258, 30, 7, 24, 16, 25},
   {1258, 24, 34, 13, 0, 0}},
  /* v23 */
  {{1364, 30, 4, 121, 5, 122},
   {1364, 28, 4, 47, 14, 48},
   {1364, 30, 11, 24, 14, 25},
   {1364, 30, 16, 15, 14, 16}},
  /* v24 */
  {{1474, 30, 6, 117, 4, 118},
   {1474, 28, 6, 45, 14, 46},
   {1474, 30, 11, 24, 16, 25},
   {1474, 30, 30, 16, 2, 17}},
  /* v25 */
  {{1588, 26, 8, 106, 4, 107},
   {1588, 28, 8, 47, 13, 48},
   {1588, 30, 7, 24, 22, 25},
   {1588, 30, 22, 15, 13, 16}},
  /* v26 */
  {{1706, 28, 10, 114, 2, 115},
   {1706, 28, 19, 46, 4, 47},
   {1706, 28, 28, 22, 6, 23},
   {1706, 30, 33, 16, 4, 17}},
  /* v27 */
  {{1828, 30, 8, 122, 4, 123},
   {1828, 28, 22, 45, 3, 46},
   {1828, 30, 8, 23, 26, 24},
   {1828, 30, 12, 15, 28, 16}},
  /* v28 */
  {{1921, 30, 3, 117, 10, 118},
   {1921, 28, 3, 45, 23, 46},
   {1921, 30, 4, 24, 31, 25},
   {1921, 30, 11, 15, 31, 16}},
  /* v29 */
  {{2051, 30, 7, 116, 7, 117},
   {2051, 28, 21, 45, 7, 46},
   {2051, 30, 1, 23, 37, 24},
   {2051, 30, 19, 15, 26, 16}},
  /* v30 */
  {{2185, 30, 5, 115, 10, 116},
   {2185, 28, 19, 47, 10, 48},
   {2185, 30, 15, 24, 25, 25},
   {2185, 30, 23, 15, 25, 16}},
  /* v31 */
  {{2323, 30, 13, 115, 3, 116},
   {2323, 28, 2, 46, 29, 47},
   {2323, 30, 42, 24, 1, 25},
   {2323, 30, 23, 15, 28, 16}},
  /* v32 */
  {{2465, 30, 17, 115, 0, 0},
   {2465, 28, 10, 46, 23, 47},
   {2465, 30, 10, 24, 35, 25},
   {2465, 30, 19, 15, 35, 16}},
  /* v33 */
  {{2611, 30, 17, 115, 1, 116},
   {2611, 28, 14, 46, 21, 47},
   {2611, 30, 29, 24, 19, 25},
   {2611, 30, 11, 15, 46, 16}},
  /* v34 */
  {{2761, 30, 13, 115, 6, 116},
   {2761, 28, 14, 46, 23, 47},
   {2761, 30, 44, 24, 7, 25},
   {2761, 30, 59, 16, 1, 17}},
  /* v35 */
  {{2876, 30, 12, 121, 7, 122},
   {2876, 28, 12, 47, 26, 48},
   {2876, 30, 39, 24, 14, 25},
   {2876, 30, 22, 15, 41, 16}},
  /* v36 */
  {{3034, 30, 6, 121, 14, 122},
   {3034, 28, 6, 47, 34, 48},
   {3034, 30, 46, 24, 10, 25},
   {3034, 30, 2, 15, 64, 16}},
  /* v37 */
  {{3196, 30, 17, 122, 4, 123},
   {3196, 28, 29, 46, 14, 47},
   {3196, 30, 49, 24, 10, 25},
   {3196, 30, 24, 15, 46, 16}},
  /* v38 */
  {{3362, 30, 4, 122, 18, 123},
   {3362, 28, 13, 46, 32, 47},
   {3362, 30, 48, 24, 14, 25},
   {3362, 30, 42, 15, 32, 16}},
  /* v39 */
  {{3532, 30, 20, 117, 4, 118},
   {3532, 28, 40, 47, 7, 48},
   {3532, 30, 43, 24, 22, 25},
   {3532, 30, 10, 15, 67, 16}},
  /* v40 */
  {{3706, 30, 19, 118, 6, 119},
   {3706, 28, 18, 47, 31, 48},
   {3706, 30, 34, 24, 34, 25},
   {3706, 30, 20, 15, 61, 16}},
};

/* ---- Reed-Solomon error correction over GF(256), primitive poly 0x11d ---- */

static uint8_t gf_exp[512];
static uint8_t gf_log[256];
static int gf_initialized = 0;

static void gf_init(void) {
  if (gf_initialized) {
    return;
  }
  int x = 1;
  for (int i = 0; i < 255; i++) {
    gf_exp[i] = (uint8_t)x;
    gf_log[x] = (uint8_t)i;
    x <<= 1;
    if (x & 0x100) {
      x ^= 0x11d;
    }
  }
  for (int i = 255; i < 512; i++) {
    gf_exp[i] = gf_exp[i - 255];
  }
  gf_initialized = 1;
}

static uint8_t gf_mul(uint8_t a, uint8_t b) {
  if (a == 0 || b == 0) {
    return 0;
  }
  return gf_exp[gf_log[a] + gf_log[b]];
}

static uint8_t gf_inv(uint8_t a) { return gf_exp[255 - gf_log[a]]; }

/* Decode an RS block of nk data codewords followed by nroots EC codewords,
   in standard polynomial order (msg[0] is highest-degree coefficient).
   Optional erasure positions (eras_pos[k] is the msg index of an erasure)
   double the per-block correction budget: 2*errors + erasures <= nroots.
   Pass eras_pos=NULL/n_eras=0 for plain errors-only decoding (corrects up
   to nroots/2 errors). Returns number of corrections (errors+erasures) made
   in place, or -1 if the block is uncorrectable. */
static int rs_decode_block_eras(
  uint8_t *msg,
  int nk,
  int nroots,
  const int *eras_pos,
  int n_eras
) {
  gf_init();
  int n = nk + nroots;
  if (nroots <= 0 || nroots > 64 || nk <= 0 || n > 300) {
    return -1;
  }
  if (n_eras < 0 || n_eras > nroots) {
    return -1;
  }

  /* Syndromes: S[i] = msg(α^i) for i in [0, nroots). */
  uint8_t syn[64];
  int nonzero = 0;
  for (int i = 0; i < nroots; i++) {
    uint8_t x = gf_exp[i];
    uint8_t s = 0;
    for (int j = 0; j < n; j++) {
      s = gf_mul(s, x) ^ msg[j];
    }
    syn[i] = s;
    if (s) {
      nonzero = 1;
    }
  }
  if (!nonzero) {
    return 0;
  }

  /* Erasure locator Λ_E(x) = ∏ (1 - α^{n-1-eras_pos[k]} x), ascending degree.
     Roots at x = α^{-(n-1-eras_pos[k])}, matching the same x = α^{-i}
     convention used by the Chien search below. */
  uint8_t lambda_e[65] = {0};
  lambda_e[0] = 1;
  int le_deg = 0;
  uint8_t seen_eras[300] = {0};
  for (int k = 0; k < n_eras; k++) {
    int e = eras_pos[k];
    if (e < 0 || e >= n) {
      return -1;
    }
    if (seen_eras[e]) {
      continue; /* dedupe duplicate erasure positions */
    }
    seen_eras[e] = 1;
    int p = (n - 1 - e) % 255;
    if (p < 0) {
      p += 255;
    }
    uint8_t a = gf_exp[p];
    if (le_deg + 1 >= 65) {
      return -1;
    }
    le_deg++;
    for (int i = le_deg; i > 0; i--) {
      lambda_e[i] ^= gf_mul(a, lambda_e[i - 1]);
    }
  }

  /* Forney syndromes T(x) = (Λ_E(x) * S(x)) mod x^nroots. */
  uint8_t Tsyn[64] = {0};
  for (int i = 0; i < nroots; i++) {
    uint8_t s = 0;
    for (int j = 0; j <= le_deg && j <= i; j++) {
      s ^= gf_mul(lambda_e[j], syn[i - j]);
    }
    Tsyn[i] = s;
  }

  /* Berlekamp-Massey on the trailing (nroots - le_deg) Forney syndromes to
     find the error locator Σ(x) for unknown-position errors. The first
     le_deg Forney coefficients are absorbed by the erasure locator. */
  uint8_t C[65] = {0};
  uint8_t B[65] = {0};
  C[0] = 1;
  B[0] = 1;
  int L = 0;
  int m = 1;
  uint8_t b_last = 1;
  int n_iter = nroots - le_deg;

  for (int k = 0; k < n_iter; k++) {
    int kk = k + le_deg;
    uint8_t delta = Tsyn[kk];
    for (int i = 1; i <= L; i++) {
      if (kk - i >= 0) {
        delta ^= gf_mul(C[i], Tsyn[kk - i]);
      }
    }
    if (delta != 0) {
      uint8_t coef = gf_mul(delta, gf_inv(b_last));
      uint8_t Tc[65];
      memcpy(Tc, C, sizeof(Tc));
      for (int i = 0; i + m < 65; i++) {
        C[i + m] ^= gf_mul(coef, B[i]);
      }
      if (2 * L <= k) {
        L = k + 1 - L;
        memcpy(B, Tc, sizeof(B));
        b_last = delta;
        m = 1;
      }
      else {
        m++;
      }
    }
    else {
      m++;
    }
  }

  if (2 * L + le_deg > nroots) {
    return -1;
  }

  /* Combined locator Ψ(x) = Λ_E(x) * Σ(x). Coefficients in [0..130). */
  uint8_t Psi[130] = {0};
  for (int i = 0; i <= le_deg; i++) {
    if (!lambda_e[i]) {
      continue;
    }
    for (int j = 0; j <= L; j++) {
      if (!C[j]) {
        continue;
      }
      Psi[i + j] ^= gf_mul(lambda_e[i], C[j]);
    }
  }
  int psi_deg = le_deg + L;
  if (psi_deg <= 0 || psi_deg > nroots) {
    return -1;
  }

  /* Chien search: combined locator roots are at x = α^{-i}. */
  int err_pos[64];
  int nerr = 0;
  for (int i = 0; i < n; i++) {
    int xi = (255 - (i % 255)) % 255;
    uint8_t v = 0;
    for (int j = 0; j <= psi_deg; j++) {
      if (Psi[j]) {
        int e = (gf_log[Psi[j]] + xi * j) % 255;
        v ^= gf_exp[e];
      }
    }
    if (v == 0) {
      if (nerr >= 64) {
        return -1;
      }
      err_pos[nerr++] = i;
    }
  }
  if (nerr != psi_deg) {
    return -1;
  }

  /* Forney: Ω(x) = (S(x) * Ψ(x)) mod x^nroots, then magnitude at position i
     is Ω(α^{-i}) / Ψ'(α^{-i}). Ψ has degree up to nroots, so omega and Ψ'
     buffers need to span that range. */
  uint8_t omega[130] = {0};
  for (int i = 0; i < nroots; i++) {
    for (int j = 0; j <= psi_deg && i + j < 130; j++) {
      if (i + j < nroots) {
        omega[i + j] ^= gf_mul(syn[i], Psi[j]);
      }
    }
  }

  uint8_t Psip[130] = {0};
  for (int i = 1; i <= psi_deg; i += 2) {
    Psip[i - 1] = Psi[i];
  }

  for (int k = 0; k < nerr; k++) {
    int i = err_pos[k];
    int xi = (255 - (i % 255)) % 255;

    uint8_t o = 0;
    for (int j = 0; j < nroots; j++) {
      if (omega[j]) {
        int e = (gf_log[omega[j]] + xi * j) % 255;
        o ^= gf_exp[e];
      }
    }

    uint8_t cp = 0;
    for (int j = 0; j < 130; j++) {
      if (Psip[j]) {
        int e = (gf_log[Psip[j]] + xi * j) % 255;
        cp ^= gf_exp[e];
      }
    }

    if (cp == 0) {
      return -1;
    }
    /* Forney for syndromes starting at b=0: e_j = X_j * Ω(X_j^{-1}) /
       Ψ'(X_j^{-1}). The X_j factor is essential — without it the recovered
       magnitude is e_j / X_j, the verify check fails, and RS decoding silently
       bails out on every block with at least one real error. */
    int xpow = i % 255;
    uint8_t correction = gf_mul(o, gf_inv(cp));
    correction = gf_mul(correction, gf_exp[xpow]);
    msg[n - 1 - i] ^= correction;
  }

  /* Verify: all syndromes zero now. */
  for (int i = 0; i < nroots; i++) {
    uint8_t x = gf_exp[i];
    uint8_t s = 0;
    for (int j = 0; j < n; j++) {
      s = gf_mul(s, x) ^ msg[j];
    }
    if (s != 0) {
      return -1;
    }
  }

  return nerr;
}

static int rs_decode_block(uint8_t *msg, int nk, int nroots) {
  return rs_decode_block_eras(msg, nk, nroots, NULL, 0);
}

/* ---- De-interleave codewords + Reed-Solomon error correction ----
   Splits the raw interleaved codeword stream into per-block [data | EC]
   buffers, runs RS decode on each block, and writes the corrected data
   codewords to `out`. Returns the total number of data codewords, or 0 on
   structural failure. Individual block RS failures are tolerated: the
   (uncorrected) bytes are still written so the payload decoder can try.
   If out_rs_cost is non-NULL, writes the total RS error-correction cost:
   sum of bits corrected across all blocks, plus a heavy penalty per
   uncorrectable block. Lower values mean a more confident decode. */
static int deinterleave(
  uint8_t const *raw,
  uint8_t const *raw_ambig,
  int raw_len,
  const ECInfo *info,
  uint8_t *out,
  int *out_rs_cost
) {
  int total_blocks = info->g1_blocks + info->g2_blocks;
  if (total_blocks == 0 || total_blocks > 100) {
    return 0;
  }
  int ec_cw = info->ec_cw_per_block;
  int total_data =
    info->g1_blocks * info->g1_data_cw + info->g2_blocks * info->g2_data_cw;
  int total_needed = total_data + total_blocks * ec_cw;
  if (total_needed > raw_len) {
    /* Not enough bytes to run RS. Fall back to copying available data only. */
    int n = total_data < raw_len ? total_data : raw_len;
    memcpy(out, raw, n);
    return n;
  }

  uint8_t **blocks = calloc(total_blocks, sizeof(uint8_t *));
  uint8_t **block_ambig = NULL;
  int *dlen = calloc(total_blocks, sizeof(int));
  if (raw_ambig) {
    block_ambig = calloc(total_blocks, sizeof(uint8_t *));
  }
  if (!blocks || !dlen || (raw_ambig && !block_ambig)) {
    free(blocks);
    free(dlen);
    free(block_ambig);
    return 0;
  }
  int alloc_fail = 0;
  for (int b = 0; b < total_blocks; b++) {
    dlen[b] = (b < info->g1_blocks) ? info->g1_data_cw : info->g2_data_cw;
    blocks[b] = calloc(dlen[b] + ec_cw, 1);
    if (block_ambig) {
      block_ambig[b] = calloc(dlen[b] + ec_cw, 1);
      if (!block_ambig[b]) {
        alloc_fail = 1;
      }
    }
    if (!blocks[b]) {
      alloc_fail = 1;
    }
  }
  if (alloc_fail) {
    for (int b = 0; b < total_blocks; b++) {
      free(blocks[b]);
      if (block_ambig) {
        free(block_ambig[b]);
      }
    }
    free(blocks);
    free(block_ambig);
    free(dlen);
    return 0;
  }

  int max_data_cw = (info->g2_blocks > 0) ? info->g2_data_cw : info->g1_data_cw;
  int pos = 0;

  /* Data codewords are interleaved round-robin by position. Group-1 blocks
     run out first (they're shorter), so later rounds only fill group-2. */
  for (int i = 0; i < max_data_cw; i++) {
    for (int b = 0; b < total_blocks && pos < raw_len; b++) {
      if (i < dlen[b]) {
        blocks[b][i] = raw[pos];
        if (block_ambig) {
          block_ambig[b][i] = raw_ambig[pos];
        }
        pos++;
      }
    }
  }
  /* EC codewords: all blocks have the same EC length, so clean round-robin. */
  for (int i = 0; i < ec_cw; i++) {
    for (int b = 0; b < total_blocks && pos < raw_len; b++) {
      blocks[b][dlen[b] + i] = raw[pos];
      if (block_ambig) {
        block_ambig[b][dlen[b] + i] = raw_ambig[pos];
      }
      pos++;
    }
  }

  /* Snapshot the original block bytes BEFORE rs_decode_block mutates them
     in place. Used to retry with erasures (and to detect when erasure
     decoding flips bytes the errors-only path "corrected" away). */
  uint8_t **orig = NULL;
  if (block_ambig) {
    orig = calloc(total_blocks, sizeof(uint8_t *));
    if (orig) {
      for (int b = 0; b < total_blocks; b++) {
        int blk_n = dlen[b] + ec_cw;
        orig[b] = malloc(blk_n);
        if (orig[b]) {
          memcpy(orig[b], blocks[b], blk_n);
        }
      }
    }
  }

  int rs_cost = 0;
  for (int b = 0; b < total_blocks; b++) {
    int blk_n = dlen[b] + ec_cw;
    int corr = rs_decode_block(blocks[b], dlen[b], ec_cw);

    if (block_ambig && orig && orig[b]) {
      /* Collect erasure candidate positions for this block. */
      int eras_pos[64];
      int n_eras = 0;
      for (int k = 0; k < blk_n && n_eras < ec_cw && n_eras < 64; k++) {
        if (block_ambig[b][k]) {
          eras_pos[n_eras++] = k;
        }
      }

      if (n_eras > 0) {
        uint8_t *retry = malloc(blk_n);
        if (retry) {
          memcpy(retry, orig[b], blk_n);
          int corr2 =
            rs_decode_block_eras(retry, dlen[b], ec_cw, eras_pos, n_eras);
          if (corr2 >= 0) {
            int prefer_erasure = 0;
            if (corr < 0) {
              /* Errors-only failed; erasure version is our only option. */
              prefer_erasure = 1;
            }
            else if (memcmp(retry, blocks[b], blk_n) != 0) {
              /* Errors-only "succeeded" but with a different answer than
                 the erasure-aware decode. The erasure decode had MORE
                 constraints (specific byte positions known to be unreliable)
                 so it's more likely to be the true codeword when our
                 erasure candidates overlap the real errors. The risk: if
                 most candidates were wrong, this flips a correct decode to
                 a wrong one — the upper-layer payload validator and
                 fmt_dist scoring is what saves us when that happens. */
              prefer_erasure = 1;
            }
            if (prefer_erasure) {
              memcpy(blocks[b], retry, blk_n);
              corr = corr2;
              rs_cost += corr2 + n_eras * 2 + 200;
              free(retry);
              continue;
            }
          }
          free(retry);
        }
      }
    }

    if (corr < 0) {
      /* Uncorrectable block: every EC symbol effectively spent without
         success. Penalise heavily so this decode loses ties to one whose
         blocks all decoded cleanly. */
      rs_cost += ec_cw * 8 + 1000;
    }
    else {
      rs_cost += corr;
    }
  }
  if (orig) {
    for (int b = 0; b < total_blocks; b++) {
      free(orig[b]);
    }
    free(orig);
  }
  if (out_rs_cost) {
    *out_rs_cost = rs_cost;
  }

  int out_pos = 0;
  for (int b = 0; b < total_blocks; b++) {
    memcpy(out + out_pos, blocks[b], dlen[b]);
    out_pos += dlen[b];
    free(blocks[b]);
    if (block_ambig) {
      free(block_ambig[b]);
    }
  }
  free(blocks);
  free(block_ambig);
  free(dlen);
  return out_pos;
}

/* ---- Payload decoding ---- */

static char *decode_payload(uint8_t const *data, int len, int ver) {
  if (len < 1) {
    return NULL;
  }
  int pos = 0;
  int mode = read_bits(data, len, &pos, 4);
  if (mode == 0) {
    return NULL;
  }

  /* ECI (Extended Channel Interpretation): consume the assignment number
     bytes and continue with the next mode indicator. URL QRs commonly
     begin with ECI 26 (UTF-8) or ECI 3 (ISO-8859-1) followed by byte
     mode. We don't do any character-set re-encoding — all the ECIs we
     care about produce payloads that are already valid UTF-8 or ASCII. */
  if (mode == 7) {
    int first = read_bits(data, len, &pos, 1);
    if (first < 0) {
      return NULL;
    }
    if (first == 0) {
      /* 1-byte ECI (7 more bits, total 8) */
      if (read_bits(data, len, &pos, 7) < 0) {
        return NULL;
      }
    }
    else {
      int second = read_bits(data, len, &pos, 1);
      if (second < 0) {
        return NULL;
      }
      if (second == 0) {
        /* 2-byte ECI (14 more bits, total 16) */
        if (read_bits(data, len, &pos, 14) < 0) {
          return NULL;
        }
      }
      else {
        /* 3-byte ECI (21 more bits, total 24) */
        if (read_bits(data, len, &pos, 21) < 0) {
          return NULL;
        }
      }
    }
    mode = read_bits(data, len, &pos, 4);
    if (mode == 0) {
      return NULL;
    }
  }

  int cc_bits;
  if (mode == 1) {
    cc_bits = (ver < 10) ? 10 : (ver < 27) ? 12 : 14;
  }
  else if (mode == 2) {
    cc_bits = (ver < 10) ? 9 : (ver < 27) ? 11 : 13;
  }
  else if (mode == 4) {
    cc_bits = (ver < 10) ? 8 : 16;
  }
  else if (mode == 8) {
    cc_bits = (ver < 10) ? 8 : (ver < 27) ? 10 : 12;
  }
  else {
    return NULL;
  }

  int count = read_bits(data, len, &pos, cc_bits);
  /* Reject impossible counts: must be positive and must fit in the data bits
     remaining after all header bits consumed so far (mode + optional ECI
     header + count). A bit-flip in the high bit of the count field can
     otherwise produce massively-inflated counts that consume padding bytes
     as fake content (numeric mode is especially prone — corrupted padding
     bytes decode as long runs of '0' digits). */
  int avail_bits = len * 8 - pos;
  if (avail_bits < 0) {
    free(NULL);
    return NULL;
  }
  int max_count;
  if (mode == 1) { /* Numeric: 10 bits per 3 digits */
    max_count = (avail_bits * 3) / 10;
  }
  else if (mode == 2) { /* Alphanumeric: 11 bits per 2 chars */
    max_count = (avail_bits * 2) / 11;
  }
  else if (mode == 4) { /* Byte: 8 bits per char */
    max_count = avail_bits / 8;
  }
  else { /* Kanji: 13 bits per char */
    max_count = avail_bits / 13;
  }
  if (count <= 0 || count > max_count) {
    return NULL;
  }

  char *result = malloc(count + 1);
  if (!result) {
    return NULL;
  }

  if (mode == 4) { /* Byte */
    for (int i = 0; i < count; i++) {
      result[i] = (char)read_bits(data, len, &pos, 8);
    }
    result[count] = '\0';
  }
  else if (mode == 2) { /* Alphanumeric */
    static const char tbl[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
    int i = 0;
    while (i + 1 < count) {
      int v = read_bits(data, len, &pos, 11);
      int hi = v / 45;
      int lo = v % 45;
      if (hi >= 45) {
        free(result);
        return NULL;
      }
      result[i] = tbl[hi];
      result[i + 1] = tbl[lo];
      i += 2;
    }
    if (i < count) {
      int v = read_bits(data, len, &pos, 6);
      if (v >= 45) {
        free(result);
        return NULL;
      }
      result[i] = tbl[v];
    }
    result[count] = '\0';
  }
  else if (mode == 1) { /* Numeric */
    int i = 0;
    while (i + 2 < count) {
      int v = read_bits(data, len, &pos, 10);
      result[i] = '0' + v / 100;
      result[i + 1] = '0' + (v / 10) % 10;
      result[i + 2] = '0' + v % 10;
      i += 3;
    }
    if (count - i == 2) {
      int v = read_bits(data, len, &pos, 7);
      result[i] = '0' + v / 10;
      result[i + 1] = '0' + v % 10;
    }
    else if (count - i == 1) {
      result[i] = '0' + read_bits(data, len, &pos, 4);
    }
    result[count] = '\0';
  }
  else {
    free(result);
    return NULL;
  }
  return result;
}

/* Decode a sampled grid into a payload string, assuming canonical
   orientation (finders at TL / TR / BL). Returns NULL on failure. Writes
   the format-code Hamming distance to *out_fmt_dist if non-NULL. Writes
   the Reed-Solomon correction cost (lower = more confident) to
   *out_rs_cost if non-NULL. */
static char *decode_grid_oriented(
  uint8_t *grid,
  uint8_t const *conf,
  int qr_size,
  int version,
  int *out_fmt_dist,
  int *out_rs_cost
) {
  /* Structural check on the three finder windows. Real QRs match the
     7x7 finder pattern at (0,0) / (0, qr_size-7) / (qr_size-7, 0) almost
     perfectly; hallucinated grids only match about half the 147 cells.
     The 0.15 cap leaves room for moderate homography drift and sampling
     jitter under blur/perspective while killing the small-module numeric
     FPs that slipped past the timing alternation. */
  float fer = finder_error_rate(grid, qr_size);
  if (fer > 0.15f) {
    return NULL;
  }
  /* Gate on the timing pattern before the expensive RS path runs. Triples
     lifted from noise almost always flunk this (alternation is random).
     Base threshold 0.22 gives real QRs under moderate blur/perspective
     plenty of headroom while nearly every hallucinated grid sits around
     0.5. When the three finder windows match their 7x7 template almost
     perfectly (fer < 0.03) we're confident the triple is real — raise
     the timing cap to 0.35 so photographs with paper curl or moderate
     perspective drift don't get rejected on what is ultimately a grid-
     sampling approximation error, not a false positive. */
  float timing_cap = (fer < 0.03f) ? 0.35f : 0.22f;
  if (timing_error_rate(grid, qr_size) > timing_cap) {
    return NULL;
  }
  int ec_level, mask_pattern;
  int fmt_dist = 16;
  if (decode_format(grid, qr_size, &ec_level, &mask_pattern, &fmt_dist) < 0) {
    return NULL;
  }
  if (out_fmt_dist) {
    *out_fmt_dist = fmt_dist;
  }
  if (out_rs_cost) {
    *out_rs_cost = 100000;
  }
  int max_bits = qr_size * qr_size;
  uint8_t *data_bits = malloc(max_bits);
  uint8_t *bit_conf = NULL;
  if (conf) {
    bit_conf = malloc(max_bits);
    if (!bit_conf) {
      free(data_bits);
      return NULL;
    }
  }
  if (!data_bits) {
    free(bit_conf);
    return NULL;
  }
  int nbits = extract_bits(
    grid,
    conf,
    qr_size,
    version,
    mask_pattern,
    data_bits,
    bit_conf,
    max_bits
  );
  int max_bytes = nbits / 8;
  uint8_t *raw_bytes = malloc(max_bytes + 1);
  uint8_t *byte_ambig = NULL;
  if (bit_conf) {
    byte_ambig = malloc(max_bytes + 1);
    if (!byte_ambig) {
      free(data_bits);
      free(bit_conf);
      free(raw_bytes);
      return NULL;
    }
  }
  if (!raw_bytes) {
    free(data_bits);
    free(bit_conf);
    free(byte_ambig);
    return NULL;
  }
  int nraw = bits_to_bytes(data_bits, bit_conf, nbits, raw_bytes, byte_ambig);
  free(data_bits);
  free(bit_conf);

  int data_cw_count = nraw;
  uint8_t *data_bytes = raw_bytes;
  uint8_t *deint_buf = NULL;
  int rs_cost = 100000;
  if (version >= 1 && version <= 40 && ec_level >= 0 && ec_level <= 3) {
    const ECInfo *info = &EC_TABLE[version - 1][ec_level];
    int total_data =
      info->g1_blocks * info->g1_data_cw + info->g2_blocks * info->g2_data_cw;
    if (total_data > 0 && total_data <= nraw) {
      deint_buf = malloc(total_data + 1);
      if (deint_buf) {
        data_cw_count =
          deinterleave(raw_bytes, byte_ambig, nraw, info, deint_buf, &rs_cost);
        data_bytes = deint_buf;
      }
      else {
        data_cw_count = total_data;
      }
    }
  }
  char *decoded = decode_payload(data_bytes, data_cw_count, version);
  free(raw_bytes);
  free(byte_ambig);
  free(deint_buf);
  if (decoded) {
    int dlen = (int)strlen(decoded);
    for (int i = 0; i < dlen; i++) {
      uint8_t ch = (uint8_t)decoded[i];
      if (ch < 0x20 || ch > 0x7e) {
        free(decoded);
        return NULL;
      }
    }
  }
  if (decoded && out_rs_cost) {
    *out_rs_cost = rs_cost;
  }
  return decoded;
}

/* Transpose an n*n byte buffer (grid[i*n+j] -> out[j*n+i]). */
static void transpose_grid_u8(uint8_t const *src, uint8_t *dst, int n) {
  for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
      dst[j * n + i] = src[i * n + j];
    }
  }
}

/* Decode a sampled grid, retrying with the matrix transposed when the
   canonical orientation fails. A "mirrored" QR (ZXing terminology: the
   symbol read as its own reflection, e.g. through a mirror or printed
   wrong-way-round) appears with rows and columns swapped relative to the
   spec. Finder and timing patterns are symmetric under transposition, so
   those structural checks pass on both orientations, but decode_format
   samples the wrong cells and the decode aborts. Transposing the grid
   (and confidence buffer) puts the format, mask, alignment patterns, and
   zigzag data reading back into canonical positions. Runs only after the
   canonical decode returns NULL, so valid non-mirrored QRs never pay for
   the retry and cannot be displaced by a spurious mirror decode. */
static char *decode_grid(
  uint8_t *grid,
  uint8_t const *conf,
  int qr_size,
  int version,
  int *out_fmt_dist,
  int *out_rs_cost
) {
  char *d = decode_grid_oriented(
    grid,
    conf,
    qr_size,
    version,
    out_fmt_dist,
    out_rs_cost
  );
  if (d) {
    return d;
  }

  int n = qr_size * qr_size;
  uint8_t *tgrid = malloc((size_t)n);
  if (!tgrid) {
    return NULL;
  }
  uint8_t *tconf = NULL;
  if (conf) {
    tconf = malloc((size_t)n);
    if (!tconf) {
      free(tgrid);
      return NULL;
    }
    transpose_grid_u8(conf, tconf, qr_size);
  }
  transpose_grid_u8(grid, tgrid, qr_size);

  int fd = 16, rc = 100000;
  char *dm = decode_grid_oriented(tgrid, tconf, qr_size, version, &fd, &rc);
  free(tgrid);
  free(tconf);
  if (dm) {
    if (out_fmt_dist) {
      *out_fmt_dist = fd;
    }
    if (out_rs_cost) {
      *out_rs_cost = rc;
    }
  }
  return dm;
}

/* ---- Decode the grid under a homography, then perturb the sample offset
   to hunt for a stronger decode ----
   The 5-point voting in sample_grid_homography_offset_mode is robust to small
   grid drift inside a single module, but if the WHOLE sampling grid is shifted
   by a fraction of a module (typical when finder centers carry sub-module error
   under blur or perspective), entire rows or columns get misread the same
   way. A 1-bit error in the byte-mode count field is the most damaging
   failure mode — it can truncate an 11-character decode to 3 characters
   while still passing the format BCH cleanly. Re-sampling at small (dx, dy)
   shifts in module space and keeping the best decode catches these
   off-by-fraction grid misalignments. */
static char *decode_with_perturbation(
  uint8_t const *pixels,
  uint8_t const *gray,
  int width,
  int height,
  const double H[9],
  int qr_size,
  int version,
  int *out_fmt_dist,
  int *out_rs_cost
) {
  /* Sample all 9 offsets (canonical + 8 perturbations) and collect the
     decodes with their format-distance scores. We then apply two acceptance
     paths: (a) if canonical succeeds, allow a strictly-longer perturbation
     that has the canonical result as a prefix to replace it (the byte-mode
     count-truncation recovery — "c3Q" -> "c3QrgLROiE5"). (b) if canonical
     fails, only accept a perturbation result that two or more perturbations
     agree on (a consensus decode), because lone perturbation decodes from
     triples whose canonical homography fails are almost always wrong same-
     length samples that other triples would decode correctly. */
#define PERT_N 10
  /* Sample plan: 8 NN perturbations + 2 canonical samples (NN + bilinear).
     Bilinear is only used at the canonical (0,0) offset — its purpose is
     specifically to recover from the byte-mode count-truncation pattern
     where NN nearest-pixel rounding clips the high bit of the count field.
     Off-canonical bilinear samples introduce too many same-length wrong
     decodes that distort the consensus vote in path (b). */
  static const double offs[PERT_N][2] = {
    {0.00, 0.00},
    {0.00, 0.00}, /* p=0 NN canonical, p=1 bilinear canonical */
    {0.30, 0.00},
    {-0.30, 0.00},
    {0.00, 0.30},
    {0.00, -0.30},
    {0.30, 0.30},
    {-0.30, 0.30},
    {0.30, -0.30},
    {-0.30, -0.30},
  };
  char *decodes[PERT_N] = {0};
  int fds[PERT_N];
  int rs_costs[PERT_N];
  size_t lens[PERT_N] = {0};
  for (int i = 0; i < PERT_N; i++) {
    fds[i] = 16;
    rs_costs[i] = 100000;
  }
  for (int p = 0; p < PERT_N; p++) {
    int use_bilinear = (p == 1);
    uint8_t *conf = NULL;
    uint8_t *g = sample_grid_homography_offset_mode(
      pixels,
      gray,
      width,
      height,
      H,
      qr_size,
      offs[p][0],
      offs[p][1],
      use_bilinear,
      gray ? &conf : NULL
    );
    if (!g) {
      free(conf);
      continue;
    }
    char *d = decode_grid(g, conf, qr_size, version, &fds[p], &rs_costs[p]);
    free(g);
    free(conf);
    if (d) {
      decodes[p] = d;
      lens[p] = strlen(d);
    }
    /* Early exit if the NN canonical (p=0) is already a long, perfect
       decode with no RS corrections — nothing can credibly improve on it. */
    if (p == 0 && d && fds[0] == 0 && lens[0] >= 8 && rs_costs[0] == 0) {
      break;
    }
  }

  char *best = NULL;
  int best_fd = 16;
  int best_rs_out = 100000;

  if (decodes[0]) {
    /* Path (a): canonical succeeded. Replace either with (i) a strictly
       longer decode that has the canonical as a prefix (the byte-mode
       count-truncation recovery), or (ii) a same-length decode whose
       Reed-Solomon correction cost is strictly lower (RS needed fewer
       fixes, which is a strong signal that the perturbation hit cleaner
       data bits even when format BCH ties at fmt_dist=0). */
    best = decodes[0];
    best_fd = fds[0];
    int best_rs = rs_costs[0];
    decodes[0] = NULL;
    for (int p = 1; p < PERT_N; p++) {
      if (!decodes[p]) {
        continue;
      }
      int replace = 0;
      if (lens[p] > strlen(best) &&
          strncmp(decodes[p], best, strlen(best)) == 0) {
        replace = 1;
      }
      else if (lens[p] == strlen(best) && rs_costs[p] < best_rs) {
        replace = 1;
      }
      if (replace) {
        free(best);
        best = decodes[p];
        best_fd = fds[p];
        best_rs = rs_costs[p];
        decodes[p] = NULL;
      }
    }
    best_rs_out = best_rs;
  }
  else {
    /* Path (b): canonical failed. Pick the perturbation decode that the
       most other perturbations agree on, requiring at least two
       agreements total. Tie-break: longer string, then lower fmt_dist.
       Without consensus, return NULL so other triples can be tried. */
    int best_idx = -1;
    int best_count = 0;
    for (int i = 1; i < PERT_N; i++) {
      if (!decodes[i]) {
        continue;
      }
      int count = 0;
      for (int j = 1; j < PERT_N; j++) {
        if (decodes[j] && strcmp(decodes[i], decodes[j]) == 0) {
          count++;
        }
      }
      int better = 0;
      if (count > best_count) {
        better = 1;
      }
      else if (count == best_count && best_idx >= 0) {
        if (lens[i] > lens[best_idx]) {
          better = 1;
        }
        else if (lens[i] == lens[best_idx] && fds[i] < fds[best_idx]) {
          better = 1;
        }
      }
      if (better) {
        best_count = count;
        best_idx = i;
      }
    }
    if (best_idx >= 0 && best_count >= 2) {
      best = decodes[best_idx];
      best_fd = fds[best_idx];
      best_rs_out = rs_costs[best_idx];
      decodes[best_idx] = NULL;
    }
  }

  for (int p = 0; p < PERT_N; p++) {
    free(decodes[p]);
  }
#undef PERT_N
  if (best) {
    if (out_fmt_dist) {
      *out_fmt_dist = best_fd;
    }
    if (out_rs_cost) {
      *out_rs_cost = best_rs_out;
    }
  }
  return best;
}

/* ---- Try to decode a single QR code from a triple ---- */

static char *try_decode_triple(
  uint8_t const *pixels,
  uint8_t const *gray,
  int width,
  int height,
  FinderPattern tl,
  FinderPattern tr,
  FinderPattern bl,
  int *out_fmt_dist,
  int *out_rs_cost,
  Corners *out_corners,
  Point2D out_finders[3],
  double *out_module_size,
  int *out_version,
  int *out_qr_size,
  Point2D out_alignments[QR_MAX_ALIGNMENTS],
  int *out_alignment_count
) {
  float dist_t = fp_dist(tl, tr), dist_l = fp_dist(tl, bl);
  float avg_d = (dist_t + dist_l) / 2.0f;
  float avg_m = (tl.module_size + tr.module_size + bl.module_size) / 3.0f;
  if (avg_m < 0.5f) {
    return NULL;
  }
  float qr_sz_f = avg_d / avg_m + 7.0f;
  int version = (int)((qr_sz_f - 21.0f + 2.0f) / 4.0f) + 1;
  if (version < 1) {
    version = 1;
  }
  if (version > 40) {
    version = 40;
  }
  int qr_size = 21 + (version - 1) * 4;

  /* Build a perspective transform for grid sampling. For v≥2 QRs we search
     for the BR alignment pattern and use its pixel location as the 4th
     homography point; under perspective distortion this beats the
     parallelogram-BR assumption that affine sampling relies on. For v1 (no
     alignment) we fall back to the parallelogram BR. */
  int fmt_dist = 16;
  int rs_cost = 100000;
  char *decoded = NULL;

  /* Track the best initial homography we have so timing-pattern refinement
     has a starting point to predict module-centroid positions from. */
  double H_primary[9];
  int have_primary_H = 0;
  int have_alignment = 0;
  double alignment_x = 0, alignment_y = 0;

  /* Track the homography (if any) associated with the winning decode so
     callers can enumerate every alignment pattern the image exposes. Not
     populated for the quadratic-warp fallback, which has no single H. */
  double winning_H[9];
  int have_winning_H = 0;

  if (version >= 2) {
    float sx0 = 3.5f, sy0 = 3.5f;
    float sx1 = qr_size - 3.5f, sy1 = 3.5f;
    float sx2 = 3.5f, sy2 = qr_size - 3.5f;
    float denom = (sx0 - sx2) * (sy1 - sy2) - (sx1 - sx2) * (sy0 - sy2);
    if (fabsf(denom) >= 1e-6f) {
      float a11 =
        ((tl.x - bl.x) * (sy1 - sy2) - (tr.x - bl.x) * (sy0 - sy2)) / denom;
      float a12 =
        ((tr.x - bl.x) * (sx0 - sx2) - (tl.x - bl.x) * (sx1 - sx2)) / denom;
      float a21 =
        ((tl.y - bl.y) * (sy1 - sy2) - (tr.y - bl.y) * (sy0 - sy2)) / denom;
      float a22 =
        ((tr.y - bl.y) * (sx0 - sx2) - (tl.y - bl.y) * (sx1 - sx2)) / denom;
      float b1 = tl.x - a11 * sx0 - a12 * sy0;
      float b2 = tl.y - a21 * sx0 - a22 * sy0;
      float amx = qr_size - 6.5f, amy = qr_size - 6.5f;
      float predicted_ax = a11 * amx + a12 * amy + b1;
      float predicted_ay = a21 * amx + a22 * amy + b2;
      /* Search radius scales with QR size: under severe perspective, the
         affine-predicted BR drift grows with finder separation — a v8
         receipt photo can have the real BR alignment 10+ modules from
         the affine prediction. Clamp so the search area stays bounded. */
      float search_r = avg_m * 10.0f;
      if (version >= 7) {
        float wide = avg_m * (float)(qr_size / 4);
        if (wide > search_r) {
          search_r = wide;
        }
        if (search_r > avg_m * 20.0f) {
          search_r = avg_m * 20.0f;
        }
      }
      float found_ax, found_ay;
      if (find_alignment_pattern(
            pixels,
            width,
            height,
            predicted_ax,
            predicted_ay,
            avg_m,
            search_r,
            &found_ax,
            &found_ay
          )) {
        have_alignment = 1;
        alignment_x = found_ax;
        alignment_y = found_ay;
        double src[4][2] = {{sx0, sy0}, {sx1, sy1}, {amx, amy}, {sx2, sy2}};
        double dst[4][2] =
          {{tl.x, tl.y}, {tr.x, tr.y}, {found_ax, found_ay}, {bl.x, bl.y}};
        double H[9];
        if (compute_homography(src, dst, H)) {
          /* Try up to two homography variants for v7+ QRs: the exact
             4-point fit from the BR snap (accurate when the snap is clean)
             and a least-squares fit over all found alignment patterns
             (accurate under paper curl or severe perspective, which
             leaves the 4-point fit drifting inside the grid). Decode with
             the BR-snap H first, then fall back to the LS-refined H if
             the primary fails. */
          double H_variants[2][9];
          memcpy(H_variants[0], H, sizeof(H));
          int n_variants = 1;
          if (version >= 7) {
            enum { MAX_AP = 45 };
            double ap_src[MAX_AP][2], ap_dst[MAX_AP][2];
            int n_ap = find_all_alignment_patterns(
              pixels,
              width,
              height,
              version,
              H,
              avg_m,
              2.0f,
              ap_src,
              ap_dst,
              MAX_AP
            );
            double ap_src_a[MAX_AP][2], ap_dst_a[MAX_AP][2];
            double H_affine[9] = {a11, a12, b1, a21, a22, b2, 0.0, 0.0, 1.0};
            int n_ap_a = find_all_alignment_patterns(
              pixels,
              width,
              height,
              version,
              H_affine,
              avg_m,
              4.0f,
              ap_src_a,
              ap_dst_a,
              MAX_AP
            );
            if (n_ap_a > n_ap) {
              n_ap = n_ap_a;
              memcpy(ap_src, ap_src_a, sizeof(ap_src_a));
              memcpy(ap_dst, ap_dst_a, sizeof(ap_dst_a));
            }
            double H_refined[9];
            memcpy(H_refined, H, sizeof(H_refined));
            int refined_ok = 0;
            for (int iter = 0; iter < 3 && n_ap >= 2; iter++) {
              double pts_src[MAX_AP + 4][2];
              double pts_dst[MAX_AP + 4][2];
              int np = 0;
              pts_src[np][0] = sx0;
              pts_src[np][1] = sy0;
              pts_dst[np][0] = tl.x;
              pts_dst[np][1] = tl.y;
              np++;
              pts_src[np][0] = sx1;
              pts_src[np][1] = sy1;
              pts_dst[np][0] = tr.x;
              pts_dst[np][1] = tr.y;
              np++;
              pts_src[np][0] = sx2;
              pts_src[np][1] = sy2;
              pts_dst[np][0] = bl.x;
              pts_dst[np][1] = bl.y;
              np++;
              for (int i = 0; i < n_ap; i++) {
                pts_src[np][0] = ap_src[i][0];
                pts_src[np][1] = ap_src[i][1];
                pts_dst[np][0] = ap_dst[i][0];
                pts_dst[np][1] = ap_dst[i][1];
                np++;
              }
              double H_ls[9];
              if (!compute_homography_ls(pts_src, pts_dst, np, H_ls)) {
                break;
              }
              /* Outlier rejection: discard any AP whose reprojection is
                 more than 1.5 modules off the fit — those are wrong snaps
                 (random dark clusters that match the 5x5 template). Real
                 APs sit within 0.5 modules of the fit. Finders anchor the
                 three known corners and are kept unconditionally. */
              double pts_src_in[MAX_AP + 4][2];
              double pts_dst_in[MAX_AP + 4][2];
              int np_in = 3;
              for (int i = 0; i < 3; i++) {
                pts_src_in[i][0] = pts_src[i][0];
                pts_src_in[i][1] = pts_src[i][1];
                pts_dst_in[i][0] = pts_dst[i][0];
                pts_dst_in[i][1] = pts_dst[i][1];
              }
              float thresh = avg_m * 1.5f;
              for (int i = 3; i < np; i++) {
                double rx, ry;
                if (!project_homography(
                      H_ls,
                      pts_src[i][0],
                      pts_src[i][1],
                      &rx,
                      &ry
                    )) {
                  continue;
                }
                double dx = rx - pts_dst[i][0];
                double dy = ry - pts_dst[i][1];
                if (dx * dx + dy * dy <= thresh * thresh) {
                  pts_src_in[np_in][0] = pts_src[i][0];
                  pts_src_in[np_in][1] = pts_src[i][1];
                  pts_dst_in[np_in][0] = pts_dst[i][0];
                  pts_dst_in[np_in][1] = pts_dst[i][1];
                  np_in++;
                }
              }
              if (np_in >= 4 && np_in < np) {
                double H_ls2[9];
                if (compute_homography_ls(
                      pts_src_in,
                      pts_dst_in,
                      np_in,
                      H_ls2
                    )) {
                  memcpy(H_ls, H_ls2, sizeof(H_ls2));
                }
              }
              memcpy(H_refined, H_ls, sizeof(H_refined));
              refined_ok = 1;
              n_ap = find_all_alignment_patterns(
                pixels,
                width,
                height,
                version,
                H_refined,
                avg_m,
                2.0f,
                ap_src,
                ap_dst,
                MAX_AP
              );
            }
            if (refined_ok) {
              memcpy(H_variants[1], H_refined, sizeof(H_refined));
              n_variants = 2;
            }
          }
          memcpy(H_primary, H, sizeof(H));
          have_primary_H = 1;
          int fd = 16;
          int rc = 100000;
          char *d = NULL;
          for (int v_idx = 0; v_idx < n_variants && !d; v_idx++) {
            fd = 16;
            rc = 100000;
            d = decode_with_perturbation(
              pixels,
              gray,
              width,
              height,
              H_variants[v_idx],
              qr_size,
              version,
              &fd,
              &rc
            );
            if (d) {
              memcpy(winning_H, H_variants[v_idx], sizeof(winning_H));
              have_winning_H = 1;
            }
          }
          /* If neither variant decoded, try each found alignment pattern
             as the 4th homography point on its own (exact 3-finders + 1-AP
             fit). Covers severe perspective where one individual AP is
             well-located but the ensemble fit gets pulled off by its
             outliers. */
          if (!d && version >= 7) {
            enum { MAX_AP = 45 };
            double ap_src[MAX_AP][2], ap_dst[MAX_AP][2];
            double H_affine[9] = {a11, a12, b1, a21, a22, b2, 0.0, 0.0, 1.0};
            int n_ap = find_all_alignment_patterns(
              pixels,
              width,
              height,
              version,
              H_affine,
              avg_m,
              8.0f,
              ap_src,
              ap_dst,
              MAX_AP
            );
            for (int ai = 0; ai < n_ap && !d; ai++) {
              double src_i[4][2] = {
                {sx0, sy0},
                {sx1, sy1},
                {ap_src[ai][0], ap_src[ai][1]},
                {sx2, sy2}
              };
              double dst_i[4][2] = {
                {tl.x, tl.y},
                {tr.x, tr.y},
                {ap_dst[ai][0], ap_dst[ai][1]},
                {bl.x, bl.y}
              };
              double H_i[9];
              if (!compute_homography(src_i, dst_i, H_i)) {
                continue;
              }
              fd = 16;
              rc = 100000;
              d = decode_with_perturbation(
                pixels,
                gray,
                width,
                height,
                H_i,
                qr_size,
                version,
                &fd,
                &rc
              );
              if (d) {
                memcpy(winning_H, H_i, sizeof(winning_H));
                have_winning_H = 1;
              }
            }
          }
          if (d) {
            decoded = d;
            fmt_dist = fd;
            rs_cost = rc;
          }
        }
        /* If the canonical alignment-based homography failed, sweep a
           small grid of alignment-point perturbations and retry. Paper curl
           and perspective foreshortening mean the real alignment pattern
           can sit several pixels from where find_alignment_pattern snaps
           to, while the homography is extremely sensitive to this 4th
           point — a few pixels at the BR alignment translate to sub-module
           drift across the data area, tripping the timing and finder
           gates even when the three finder positions are pin-accurate.
           Sweep radius scales with module size (the residual alignment
           uncertainty is always a fraction of a module); 2x2 module
           radius at half-module steps is cheap (25 samples) and covers
           the drift seen on moderately curled photos. */
        if (!decoded) {
          float step = avg_m * 0.5f;
          float radius = avg_m * 1.5f;
          int n_steps = (int)(radius / step);
          char *best_sweep = NULL;
          int best_sweep_fd = 16;
          int best_sweep_rc = 100000;
          double best_sweep_H[9];
          int have_best_sweep_H = 0;
          for (int dy = -n_steps; dy <= n_steps; dy++) {
            for (int dx = -n_steps; dx <= n_steps; dx++) {
              if (dx == 0 && dy == 0) {
                continue; /* already tried */
              }
              double pert_dst[4][2] = {
                {tl.x, tl.y},
                {tr.x, tr.y},
                {found_ax + dx * step, found_ay + dy * step},
                {bl.x, bl.y}
              };
              double Hp[9];
              if (!compute_homography(src, pert_dst, Hp)) {
                continue;
              }
              int fd = 16;
              int rc = 100000;
              char *d = decode_with_perturbation(
                pixels,
                gray,
                width,
                height,
                Hp,
                qr_size,
                version,
                &fd,
                &rc
              );
              if (!d) {
                continue;
              }
              /* Take the lowest-cost decode across the whole sweep rather
                 than stopping on the first hit — some perturbed alignments
                 yield a consensus decode that is still garbage (random
                 alphanumeric strings with fd=0 and rc=0 do occur when
                 sampling happens to line up with a false-but-self-
                 consistent grid). Prefer longer strings and lower RS
                 cost so the best geometric fit wins. */
              int replace = 0;
              if (!best_sweep) {
                replace = 1;
              }
              else if (fd < best_sweep_fd) {
                replace = 1;
              }
              else if (fd == best_sweep_fd &&
                       (int)strlen(d) > (int)strlen(best_sweep)) {
                replace = 1;
              }
              else if (fd == best_sweep_fd &&
                       (int)strlen(d) == (int)strlen(best_sweep) &&
                       rc < best_sweep_rc) {
                replace = 1;
              }
              if (replace) {
                free(best_sweep);
                best_sweep = d;
                best_sweep_fd = fd;
                best_sweep_rc = rc;
                memcpy(best_sweep_H, Hp, sizeof(best_sweep_H));
                have_best_sweep_H = 1;
              }
              else {
                free(d);
              }
            }
          }
          /* Only accept sweep results with a strong confidence signal:
             perfect format BCH (fd = 0) and near-zero RS correction cost.
             Random perturbations in a QR-free region can consensus-
             decode as alphanumeric garbage with fd=0 but substantial RS
             cost (≥ 100), so locking the rc threshold low rejects those.
             Real decodes with the right alignment typically need zero
             corrections; a small tolerance (≤ 5) lets through slightly
             noisy captures without admitting hallucinated decodes. */
          if (best_sweep && best_sweep_fd == 0 && best_sweep_rc <= 5) {
            decoded = best_sweep;
            fmt_dist = best_sweep_fd;
            rs_cost = best_sweep_rc;
            if (have_best_sweep_H) {
              memcpy(winning_H, best_sweep_H, sizeof(winning_H));
              have_winning_H = 1;
            }
          }
          else {
            free(best_sweep);
          }
        }
      }
    }
  }

  /* If no homography-based decode worked yet, try the plain parallelogram
     BR (affine-equivalent). */
  if (!decoded) {
    float br_x = tr.x + bl.x - tl.x;
    float br_y = tr.y + bl.y - tl.y;
    double src[4][2] = {
      {3.5, 3.5},
      {qr_size - 3.5, 3.5},
      {qr_size - 3.5, qr_size - 3.5},
      {3.5, qr_size - 3.5}
    };
    double dst[4][2] = {{tl.x, tl.y}, {tr.x, tr.y}, {br_x, br_y}, {bl.x, bl.y}};
    double H[9];
    if (compute_homography(src, dst, H)) {
      if (!have_primary_H) {
        memcpy(H_primary, H, sizeof(H));
        have_primary_H = 1;
      }
      int fd = 16;
      int rc = 100000;
      char *d = decode_with_perturbation(
        pixels,
        gray,
        width,
        height,
        H,
        qr_size,
        version,
        &fd,
        &rc
      );
      if (d) {
        decoded = d;
        fmt_dist = fd;
        rs_cost = rc;
        memcpy(winning_H, H, sizeof(winning_H));
        have_winning_H = 1;
      }
    }
  }

  /* Timing-pattern homography refinement. When a 4-point homography (from
     alignment pattern or parallelogram BR) can't produce a clean decode,
     the usual cause on photos with any perspective or paper curl is
     sub-module drift in the QR's interior — the 4 control points anchor
     the corners but nothing constrains the transform *between* them.
     Observing pixel-space centroids of every known-dark timing module
     gives us ≈ 2·(qr_size − 16) extra correspondences threading the
     code's interior, and solving an over-determined DLT over the union
     of control points + timing observations averages out the distortion
     the 4-point fit can't model. Run it only after the canonical paths
     have failed (it adds real cost: ~2·qr_size centroid searches + an
     8×8 normal-equation solve + another 10-perturbation decode pass). */
  if (!decoded && gray && have_primary_H && version >= 2) {
    double H_cur[9];
    memcpy(H_cur, H_primary, sizeof(H_cur));
    /* Iterate refinement a few times — each pass re-predicts timing-
       module positions through the currently best H, re-observes
       centroids in the pixel buffer, and re-fits. Three rounds is
       usually enough to converge; diminishing returns beyond that. */
    for (int iter = 0; iter < 3 && !decoded; iter++) {
      double H_ref[9];
      if (!refine_h_with_timing(
            gray,
            width,
            height,
            H_cur,
            qr_size,
            avg_m,
            tl,
            tr,
            bl,
            have_alignment,
            alignment_x,
            alignment_y,
            H_ref
          )) {
        break;
      }
      memcpy(H_cur, H_ref, sizeof(H_cur));
      int fd = 16;
      int rc = 100000;
      char *d = decode_with_perturbation(
        pixels,
        gray,
        width,
        height,
        H_ref,
        qr_size,
        version,
        &fd,
        &rc
      );
      if (d) {
        decoded = d;
        fmt_dist = fd;
        rs_cost = rc;
        memcpy(winning_H, H_ref, sizeof(winning_H));
        have_winning_H = 1;
      }
    }

    /* Last-resort attempt: quadratic (polynomial) warp sampling. When
       every homography we can build still fails to decode, the surface
       itself isn't planar — typical case is a QR printed on a curled
       receipt. Fit a bivariate degree-2 polynomial to 3 finders +
       alignment + timing observations and sample the grid with it. The
       quadratic warp can follow a cylindrical bend that no single
       homography can model. Only used when the refined H's decode
       failed, so the added cost is bounded to the hardest images. */
    if (!decoded) {
      int fd = 16;
      int rc = 100000;
      char *d = try_decode_quadratic(
        pixels,
        gray,
        width,
        height,
        qr_size,
        version,
        tl,
        tr,
        bl,
        have_alignment,
        alignment_x,
        alignment_y,
        H_cur,
        avg_m,
        &fd,
        &rc
      );
      if (d) {
        decoded = d;
        fmt_dist = fd;
        rs_cost = rc;
        /* Quadratic warp has no homography; H_cur from the last timing
           refinement is the closest planar approximation. Good enough to
           seed alignment-pattern search — find_all_alignment_patterns
           snaps to the pixel-space pattern anyway. */
        memcpy(winning_H, H_cur, sizeof(winning_H));
        have_winning_H = 1;
      }
    }
  }

  if (!decoded) {
    return NULL;
  }
  if (out_fmt_dist) {
    *out_fmt_dist = fmt_dist;
  }
  if (out_rs_cost) {
    *out_rs_cost = rs_cost;
  }

  {
    /* Compute QR code outer corners from finder pattern centers.
       Finder centers sit at module (3.5, 3.5) from their corners. */
    if (out_corners) {
      float ux = (tr.x - tl.x) / (qr_size - 7.0f);
      float uy = (tr.y - tl.y) / (qr_size - 7.0f);
      float vx = (bl.x - tl.x) / (qr_size - 7.0f);
      float vy = (bl.y - tl.y) / (qr_size - 7.0f);
      out_corners->tl_x = tl.x - 3.5f * (ux + vx);
      out_corners->tl_y = tl.y - 3.5f * (uy + vy);
      out_corners->tr_x = tr.x + 3.5f * (ux - vx);
      out_corners->tr_y = tr.y + 3.5f * (uy - vy);
      float br_center_x = tr.x + bl.x - tl.x;
      float br_center_y = tr.y + bl.y - tl.y;
      out_corners->br_x = br_center_x + 3.5f * (ux + vx);
      out_corners->br_y = br_center_y + 3.5f * (uy + vy);
      out_corners->bl_x = bl.x - 3.5f * (ux - vx);
      out_corners->bl_y = bl.y - 3.5f * (uy - vy);
    }
    if (out_finders) {
      out_finders[0].x = tl.x;
      out_finders[0].y = tl.y;
      out_finders[1].x = tr.x;
      out_finders[1].y = tr.y;
      out_finders[2].x = bl.x;
      out_finders[2].y = bl.y;
    }
    if (out_module_size) {
      float ux = (tr.x - tl.x) / (qr_size - 7.0f);
      float uy = (tr.y - tl.y) / (qr_size - 7.0f);
      float vx = (bl.x - tl.x) / (qr_size - 7.0f);
      float vy = (bl.y - tl.y) / (qr_size - 7.0f);
      float mu = sqrtf(ux * ux + uy * uy);
      float mv = sqrtf(vx * vx + vy * vy);
      *out_module_size = (mu + mv) / 2.0f;
    }
    if (out_version) {
      *out_version = version;
    }
    if (out_qr_size) {
      *out_qr_size = qr_size;
    }
    if (out_alignment_count) {
      *out_alignment_count = 0;
    }
    /* Enumerate every alignment pattern the decoder could locate in the
       binarized buffer so callers (e.g. qr_draw) can visualise them.
       v1 has none; v2–6 have 1 (BR); v7+ have a spec-defined grid. Uses
       the outer-corners homography as the prediction frame — find_all_*
       snaps to the actual dark 5x5 pattern in pixel space, so prediction
       accuracy only has to put us within search_r of the true center. */
    if (out_alignments && out_alignment_count && version >= 2 && out_corners) {
      double src[4][2] = {
        {0.0, 0.0},
        {(double)qr_size, 0.0},
        {(double)qr_size, (double)qr_size},
        {0.0, (double)qr_size}
      };
      double dst[4][2] = {
        {out_corners->tl_x, out_corners->tl_y},
        {out_corners->tr_x, out_corners->tr_y},
        {out_corners->br_x, out_corners->br_y},
        {out_corners->bl_x, out_corners->bl_y}
      };
      double H[9];
      if (have_winning_H) {
        memcpy(H, winning_H, sizeof(H));
      }
      if (have_winning_H || compute_homography(src, dst, H)) {
        /* Enumerate every spec-defined alignment-pattern grid position
           for this version, projecting each module-space centre through
           H to predict its pixel location. Then refine each prediction
           against the binarized buffer if find_alignment_pattern can
           snap to a real 5x5 template — otherwise keep the prediction
           (useful for qr_draw even when the binarization degraded the
           pattern enough that the snap fails). */
        int nvals = QR_ALIGN_GRID[version][0];
        int first = (nvals >= 1) ? QR_ALIGN_GRID[version][1] : 0;
        int last = (nvals >= 1) ? QR_ALIGN_GRID[version][nvals] : 0;
        int n_out = 0;
        for (int ri = 0; ri < nvals && n_out < QR_MAX_ALIGNMENTS; ri++) {
          int row = QR_ALIGN_GRID[version][1 + ri];
          for (int ci = 0; ci < nvals && n_out < QR_MAX_ALIGNMENTS; ci++) {
            int col = QR_ALIGN_GRID[version][1 + ci];
            if ((row == first && col == first) ||
                (row == first && col == last) ||
                (row == last && col == first)) {
              continue;
            }
            double mx = (double)col + 0.5;
            double my = (double)row + 0.5;
            double pred_x, pred_y;
            if (!project_homography(H, mx, my, &pred_x, &pred_y)) {
              continue;
            }
            float snap_x = (float)pred_x, snap_y = (float)pred_y;
            /* 5-module search radius is the same tolerance used elsewhere
               in the decoder for paper-curled captures where the
               module-size estimate can drift up to ±30% locally. */
            find_alignment_pattern(
              pixels,
              width,
              height,
              (float)pred_x,
              (float)pred_y,
              (float)(*out_module_size),
              (float)(*out_module_size) * 5.0f,
              &snap_x,
              &snap_y
            );
            out_alignments[n_out].x = snap_x;
            out_alignments[n_out].y = snap_y;
            n_out++;
          }
        }
        *out_alignment_count = n_out;
      }
    }
  }
  return decoded;
}

/* ---- Main API ---- */

/* Run the finder + decode pipeline on a single binary buffer.
   best_* are initialized by the caller and refined across multiple attempts
   (e.g. one attempt per binarization strategy). gray is passed for
   subpixel finder-center refinement. */
static void try_pipeline(
  uint8_t const *bin,
  uint8_t const *gray,
  int w,
  int h,
  char **best_decoded,
  int *best_fmt_dist,
  int *best_rs_cost,
  Corners *best_corners,
  Point2D best_finders[3],
  double *best_module_size,
  int *best_version,
  int *best_qr_size,
  Point2D best_alignments[QR_MAX_ALIGNMENTS],
  int *best_alignment_count
) {
  FinderPattern fps[MAX_CANDIDATES];
  int nfp = find_finders(bin, w, h, fps, MAX_CANDIDATES);
  if (nfp < 3) {
    return;
  }
  for (int i = 0; i < nfp; i++) {
    refine_finder_center(gray, w, h, &fps[i]);
  }

  FinderPattern triples[MAX_TRIPLES][3];
  int n_triples = select_and_rank(bin, w, h, fps, nfp, triples, MAX_TRIPLES);

  for (int t = 0; t < n_triples; t++) {
    int fmt_dist = 16;
    int rs_cost = 100000;
    Corners corners = {0};
    Point2D finders[3];
    double module_size = 0;
    int version = 0;
    int qr_size = 0;
    Point2D alignments[QR_MAX_ALIGNMENTS];
    int alignment_count = 0;
    char *decoded = try_decode_triple(
      bin,
      gray,
      w,
      h,
      triples[t][0],
      triples[t][1],
      triples[t][2],
      &fmt_dist,
      &rs_cost,
      &corners,
      finders,
      &module_size,
      &version,
      &qr_size,
      alignments,
      &alignment_count
    );
    if (decoded) {
      /* Composite score: lower fmt_dist wins, tie-break first on decoded
         length (longer decodes mean a larger byte-mode count field
         survived), then on Reed-Solomon cost (lower = the data block
         decoded with fewer error corrections, which discriminates between
         two "Mz4Y9X" candidates where one has clean RS and the other is
         RS-uncorrectable but happens to share the same fmt_dist). */
      int dlen = (int)strlen(decoded);
      int replace = 0;
      if (fmt_dist < *best_fmt_dist) {
        replace = 1;
      }
      else if (fmt_dist == *best_fmt_dist && *best_decoded != NULL) {
        int prev_len = (int)strlen(*best_decoded);
        if (dlen > prev_len) {
          replace = 1;
        }
        else if (dlen == prev_len && rs_cost < *best_rs_cost) {
          replace = 1;
        }
      }
      if (replace) {
        free(*best_decoded);
        *best_decoded = decoded;
        *best_fmt_dist = fmt_dist;
        *best_rs_cost = rs_cost;
        *best_corners = corners;
        for (int i = 0; i < 3; i++) {
          best_finders[i] = finders[i];
        }
        *best_module_size = module_size;
        *best_version = version;
        *best_qr_size = qr_size;
        *best_alignment_count = alignment_count;
        for (int i = 0; i < alignment_count; i++) {
          best_alignments[i] = alignments[i];
        }
      }
      else {
        free(decoded);
      }
    }
  }
}

/* Runs the full binarizer/preprocessor cascade against `gray_pixels`. The
   sequence is cheapest-first; each attempt short-circuits once a
   high-confidence decode (fmt_dist <= 1, length >= 5) is in hand, so the
   work only expands as far as the image demands. Called by the public
   decoder at each pyramid level and once again on the inverted buffer —
   which lets light-on-dark QRs (level_13) ride the same Sauvola/upsample/
   median passes the normal-polarity pass has. `is_finest_level` gates the
   native-resolution-only passes (2x/3x upsample), which exist to rescue
   tiny QRs at native scale; upsampling from a downsampled pyramid level
   is strictly worse than running those passes at level 0. */
static void run_all_attempts(
  uint8_t const *gray_pixels,
  int w,
  int h,
  int is_finest_level,
  char **best_decoded,
  int *best_fmt_dist,
  int *best_rs_cost,
  Corners *best_corners,
  Point2D best_finders[3],
  double *best_module_size,
  int *best_version,
  int *best_qr_size,
  Point2D best_alignments[QR_MAX_ALIGNMENTS],
  int *best_alignment_count
) {
  /* Attempt 1: Otsu on original. */
  uint8_t *bin = binarize_global_otsu(gray_pixels, w, h);
  if (bin) {
    try_pipeline(
      bin,
      gray_pixels,
      w,
      h,
      best_decoded,
      best_fmt_dist,
      best_rs_cost,
      best_corners,
      best_finders,
      best_module_size,
      best_version,
      best_qr_size,
      best_alignments,
      best_alignment_count
    );
    free(bin);
  }

  /* Attempt 2: zxing-cpp hybrid on original. Sparse-threshold per 8x8 block
     plus 5x5 neighbour smoothing — low-contrast interior blocks inherit a
     threshold from contrasty edge blocks instead of falling back to a
     whole-image Otsu. Primary recovery path for faded / pastel palettes. */
  if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
    bin = binarize_hybrid(gray_pixels, w, h);
    if (bin) {
      try_pipeline(
        bin,
        gray_pixels,
        w,
        h,
        best_decoded,
        best_fmt_dist,
        best_rs_cost,
        best_corners,
        best_finders,
        best_module_size,
        best_version,
        best_qr_size,
        best_alignments,
        best_alignment_count
      );
      free(bin);
    }
  }

  /* Attempt 3: adaptive on original. */
  if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
    bin = binarize_adaptive(gray_pixels, w, h);
    if (bin) {
      try_pipeline(
        bin,
        gray_pixels,
        w,
        h,
        best_decoded,
        best_fmt_dist,
        best_rs_cost,
        best_corners,
        best_finders,
        best_module_size,
        best_version,
        best_qr_size,
        best_alignments,
        best_alignment_count
      );
      free(bin);
    }
  }

  /* The remaining attempts (Sauvola, unsharp, upsample) target large,
     visually-degraded photographs. On tiny synthetic buffers they are more
     likely to hallucinate a decode from noise than to recover a real QR
     — anything smaller than ~100 px on the short side cannot plausibly
     hold a decodable v1 QR (≥ ~5 px/module × 21 modules + quiet zone). */
  int small_image = (w < 100 || h < 100);

  /* Attempt 3: Sauvola on original. Targets faded / low-contrast modules
     where adaptive's fixed min_std fallback misclassifies. */
  if (!small_image &&
      !(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
    bin = binarize_sauvola(gray_pixels, w, h);
    if (bin) {
      try_pipeline(
        bin,
        gray_pixels,
        w,
        h,
        best_decoded,
        best_fmt_dist,
        best_rs_cost,
        best_corners,
        best_finders,
        best_module_size,
        best_version,
        best_qr_size,
        best_alignments,
        best_alignment_count
      );
      free(bin);
    }
  }

  /* Attempt 4: unsharp mask + Otsu, then unsharp mask + adaptive. Targets
     blurred QRs (level_4/5 generators add Gaussian blur radius up to 1.8). */
  if (!small_image &&
      !(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5)) {
    uint8_t *sharp = unsharp_mask_gray(gray_pixels, w, h);
    if (sharp) {
      bin = binarize_global_otsu(sharp, w, h);
      if (bin) {
        try_pipeline(
          bin,
          sharp,
          w,
          h,
          best_decoded,
          best_fmt_dist,
          best_rs_cost,
          best_corners,
          best_finders,
          best_module_size,
          best_version,
          best_qr_size,
          best_alignments,
          best_alignment_count
        );
        free(bin);
      }
      if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5
          )) {
        bin = binarize_adaptive(sharp, w, h);
        if (bin) {
          try_pipeline(
            bin,
            sharp,
            w,
            h,
            best_decoded,
            best_fmt_dist,
            best_rs_cost,
            best_corners,
            best_finders,
            best_module_size,
            best_version,
            best_qr_size,
            best_alignments,
            best_alignment_count
          );
          free(bin);
        }
      }
      free(sharp);
    }
  }

  /* Attempt 5: 2x bilinear upsample + Otsu, then upsample + adaptive.
     Targets tiny QRs whose modules are too small for finder ratio checks
     at native resolution. Coordinates from any winning upsampled decode are
     scaled back by 1/n before returning. Attempt 5b (3x) follows when 2x
     still doesn't yield a confident decode; helpful for QRs with mod < 3 px
     where even doubled modules remain on the edge of the ratio tolerance.
     Skipped at coarser pyramid levels: upsampling a downsampled buffer is
     strictly worse than running this at level 0 on the original data. */
  for (int n_try = 2; n_try <= 3; n_try++) {
    if (small_image || !is_finest_level) {
      break;
    }
    /* Skip the upscaled passes when we already have a long, perfect-format
       decode at the native scale; otherwise always try, because a tiny QR
       (modules < 4 px) cannot reliably decode at native resolution and a
       length-5 wrong decode (e.g. "Mz4Y9c" with fd=0 + uncorrectable RS)
       must not block 3x exploration that may surface "Mz4Y9a". */
    if (*best_decoded && *best_fmt_dist == 0 && strlen(*best_decoded) >= 10) {
      break;
    }
    int wn = w * n_try;
    int hn = h * n_try;
    uint8_t *grayn = upsample_nx_bilinear(gray_pixels, w, h, n_try);
    if (!grayn) {
      continue;
    }
    char *best_decoded_up = NULL;
    Corners best_corners_up = {0};
    Point2D best_finders_up[3] = {{0, 0}, {0, 0}, {0, 0}};
    double best_module_size_up = 0;
    int best_fmt_dist_up = 16;
    int best_rs_cost_up = 100000;
    int best_version_up = 0;
    int best_qr_size_up = 0;
    Point2D best_alignments_up[QR_MAX_ALIGNMENTS];
    int best_alignment_count_up = 0;

    bin = binarize_global_otsu(grayn, wn, hn);
    if (bin) {
      try_pipeline(
        bin,
        grayn,
        wn,
        hn,
        &best_decoded_up,
        &best_fmt_dist_up,
        &best_rs_cost_up,
        &best_corners_up,
        best_finders_up,
        &best_module_size_up,
        &best_version_up,
        &best_qr_size_up,
        best_alignments_up,
        &best_alignment_count_up
      );
      free(bin);
    }
    if (!(best_decoded_up && best_fmt_dist_up <= 1)) {
      bin = binarize_adaptive(grayn, wn, hn);
      if (bin) {
        try_pipeline(
          bin,
          grayn,
          wn,
          hn,
          &best_decoded_up,
          &best_fmt_dist_up,
          &best_rs_cost_up,
          &best_corners_up,
          best_finders_up,
          &best_module_size_up,
          &best_version_up,
          &best_qr_size_up,
          best_alignments_up,
          &best_alignment_count_up
        );
        free(bin);
      }
    }
    free(grayn);

    /* Promote the upsampled result if it improves on what we already have:
       lower fmt_dist wins, tie-break on longer decode length, then on
       lower RS cost. Mirrors the scoring inside try_pipeline. Scale
       spatial outputs back to native coordinates by 1/n. */
    if (best_decoded_up) {
      int replace = 0;
      int prev_len = (*best_decoded) ? (int)strlen(*best_decoded) : -1;
      int new_len = (int)strlen(best_decoded_up);
      if (!*best_decoded) {
        replace = 1;
      }
      else if (best_fmt_dist_up < *best_fmt_dist) {
        replace = 1;
      }
      else if (best_fmt_dist_up == *best_fmt_dist && new_len > prev_len) {
        replace = 1;
      }
      else if (best_fmt_dist_up == *best_fmt_dist && new_len == prev_len &&
               best_rs_cost_up < *best_rs_cost) {
        replace = 1;
      }
      if (replace) {
        double inv = 1.0 / (double)n_try;
        free(*best_decoded);
        *best_decoded = best_decoded_up;
        *best_fmt_dist = best_fmt_dist_up;
        *best_rs_cost = best_rs_cost_up;
        best_corners->tl_x = best_corners_up.tl_x * inv;
        best_corners->tl_y = best_corners_up.tl_y * inv;
        best_corners->tr_x = best_corners_up.tr_x * inv;
        best_corners->tr_y = best_corners_up.tr_y * inv;
        best_corners->br_x = best_corners_up.br_x * inv;
        best_corners->br_y = best_corners_up.br_y * inv;
        best_corners->bl_x = best_corners_up.bl_x * inv;
        best_corners->bl_y = best_corners_up.bl_y * inv;
        for (int i = 0; i < 3; i++) {
          best_finders[i].x = best_finders_up[i].x * inv;
          best_finders[i].y = best_finders_up[i].y * inv;
        }
        *best_module_size = best_module_size_up * inv;
        *best_version = best_version_up;
        *best_qr_size = best_qr_size_up;
        *best_alignment_count = best_alignment_count_up;
        for (int i = 0; i < best_alignment_count_up; i++) {
          best_alignments[i].x = best_alignments_up[i].x * inv;
          best_alignments[i].y = best_alignments_up[i].y * inv;
        }
      }
      else {
        free(best_decoded_up);
      }
    }
  }

  /* Attempt 6: 3x3 median filter + Otsu, then median + adaptive. Median
     denoising preserves edges while removing salt-and-pepper noise that
     defeats both finder-ratio checks and module-center voting. Especially
     helpful for level_5 images (sigma 4-9 noise + low contrast). Runs
     when nothing decoded OR the existing decode is suspiciously short
     (a count-field truncation of byte mode); a long decode from earlier
     is trusted and we don't risk displacing it. */
  int existing_short = *best_decoded && strlen(*best_decoded) < 8;
  if (!small_image && (!*best_decoded || existing_short)) {
    uint8_t *med = median_filter_3x3(gray_pixels, w, h);
    if (med) {
      bin = binarize_global_otsu(med, w, h);
      if (bin) {
        try_pipeline(
          bin,
          med,
          w,
          h,
          best_decoded,
          best_fmt_dist,
          best_rs_cost,
          best_corners,
          best_finders,
          best_module_size,
          best_version,
          best_qr_size,
          best_alignments,
          best_alignment_count
        );
        free(bin);
      }
      if (!(*best_decoded && *best_fmt_dist <= 1 && strlen(*best_decoded) >= 5
          )) {
        bin = binarize_adaptive(med, w, h);
        if (bin) {
          try_pipeline(
            bin,
            med,
            w,
            h,
            best_decoded,
            best_fmt_dist,
            best_rs_cost,
            best_corners,
            best_finders,
            best_module_size,
            best_version,
            best_qr_size,
            best_alignments,
            best_alignment_count
          );
          free(bin);
        }
      }
      free(med);
    }
  }
}

/* ---- Pyramid-level decode helpers ---- */

/* Scales all spatial fields of a decode state by `scale`. Used to convert
   coordinates from a downsampled pyramid level back into original-image
   pixel space. */
static void scale_decode_state(
  Corners *corners,
  Point2D finders[3],
  double *module_size,
  Point2D alignments[QR_MAX_ALIGNMENTS],
  int alignment_count,
  double scale
) {
  corners->tl_x *= scale;
  corners->tl_y *= scale;
  corners->tr_x *= scale;
  corners->tr_y *= scale;
  corners->br_x *= scale;
  corners->br_y *= scale;
  corners->bl_x *= scale;
  corners->bl_y *= scale;
  for (int i = 0; i < 3; i++) {
    finders[i].x *= scale;
    finders[i].y *= scale;
  }
  for (int i = 0; i < alignment_count; i++) {
    alignments[i].x *= scale;
    alignments[i].y *= scale;
  }
  *module_size *= scale;
}

/* Returns 1 if (cand_*) should replace (cur_*). Ranking is: lower fmt_dist
   first, then longer decoded string, then lower RS cost. Matches the
   promotion logic previously inlined in the upsample / downsample blocks. */
static int decode_is_better(
  char const *cur,
  int cur_fmt,
  int cur_rs,
  char const *cand,
  int cand_fmt,
  int cand_rs
) {
  if (!cand) {
    return 0;
  }
  if (!cur) {
    return 1;
  }
  if (cand_fmt != cur_fmt) {
    return cand_fmt < cur_fmt;
  }
  int cur_len = (int)strlen(cur);
  int cand_len = (int)strlen(cand);
  if (cand_len != cur_len) {
    return cand_len > cur_len;
  }
  return cand_rs < cur_rs;
}

FCVQRCodeResult fcv_decode_qr_codes(
  uint32_t width,
  uint32_t height,
  uint8_t const *const gray_pixels
) {
  FCVQRCodeResult result;
  result.codes = NULL;
  result.count = 0;

  if (!gray_pixels || width == 0 || height == 0) {
    return result;
  }
  if (width > INT32_MAX || height > INT32_MAX) {
    return result;
  }

  result.codes = malloc(MAX_QR_CODES * sizeof(FCVQRCode));
  if (!result.codes) {
    return result;
  }

  int w = (int)width;
  int h = (int)height;

  /* ---- Build a box-averaged pyramid ----
     Level 0 is the original; level k+1 is a 2x2 box-average of level k.
     We stop halving once the short side would drop below MIN_SHORT_SIDE,
     below which even a version-1 QR (21 modules) can't plausibly survive
     binarization (<6 px/module after quiet zone). */
#define QR_PYRAMID_MAX 6
#define QR_PYRAMID_MIN_SHORT 120
#define QR_PYRAMID_TARGET_SHORT 640

  uint8_t const *lv_px[QR_PYRAMID_MAX];
  int lv_w[QR_PYRAMID_MAX], lv_h[QR_PYRAMID_MAX];
  int lv_owned[QR_PYRAMID_MAX];

  lv_px[0] = gray_pixels;
  lv_w[0] = w;
  lv_h[0] = h;
  lv_owned[0] = 0;
  int n_levels = 1;
  while (n_levels < QR_PYRAMID_MAX) {
    int nw = lv_w[n_levels - 1] / 2;
    int nh = lv_h[n_levels - 1] / 2;
    int short_side = nw < nh ? nw : nh;
    if (short_side < QR_PYRAMID_MIN_SHORT) {
      break;
    }
    uint8_t *down = downsample_2x_box(
      lv_px[n_levels - 1],
      lv_w[n_levels - 1],
      lv_h[n_levels - 1]
    );
    if (!down) {
      break;
    }
    lv_px[n_levels] = down;
    lv_w[n_levels] = nw;
    lv_h[n_levels] = nh;
    lv_owned[n_levels] = 1;
    n_levels++;
  }

  /* Target level: the coarsest (highest-index) level whose short side is
     still >= TARGET. Decouples first-pass work from sensor resolution:
     the common case of "one QR filling a large fraction of a phone photo"
     gets scanned at ~640 px regardless of whether the source is 1 MP or
     48 MP. If no level meets the target (small input), target stays 0. */
  int target = 0;
  for (int i = 1; i < n_levels; i++) {
    int short_side = lv_w[i] < lv_h[i] ? lv_w[i] : lv_h[i];
    if (short_side >= QR_PYRAMID_TARGET_SHORT) {
      target = i;
    }
    else {
      break;
    }
  }

  char *best_decoded = NULL;
  Corners best_corners = {0};
  Point2D best_finders[3] = {{0, 0}, {0, 0}, {0, 0}};
  double best_module_size = 0;
  int best_fmt_dist = 16;
  int best_rs_cost = 100000;
  int best_version = 0;
  int best_qr_size = 0;
  Point2D best_alignments[QR_MAX_ALIGNMENTS];
  int best_alignment_count = 0;

  /* Visit order: target first (best balance of detail vs. noise for the
     typical "one QR filling most of a phone photo" case), then coarser
     levels (cheap, different binarizer response — catches level_13's
     low-contrast pastel-on-checkered case the old inline 2x-downsample
     attempt used to handle), then finer levels (expensive, last resort
     for tiny QRs). Stop as soon as a confident decode (fmt_dist <= 1,
     length >= 5) is in hand. Non-confident decodes are kept as best-so-
     far and may be displaced by a cleaner result from another level. */
  int visit_order[QR_PYRAMID_MAX];
  int n_visits = 0;
  visit_order[n_visits++] = target;
  for (int lvl = target + 1; lvl < n_levels; lvl++) {
    visit_order[n_visits++] = lvl;
  }
  for (int lvl = target - 1; lvl >= 0; lvl--) {
    visit_order[n_visits++] = lvl;
  }

  for (int vi = 0; vi < n_visits; vi++) {
    int lvl = visit_order[vi];
    double scale = (double)(1 << lvl);
    int is_finest = (lvl == 0);
    int lw = lv_w[lvl];
    int lh = lv_h[lvl];
    uint8_t const *lpx = lv_px[lvl];

    char *decoded = NULL;
    Corners corners = {0};
    Point2D finders[3] = {{0, 0}, {0, 0}, {0, 0}};
    double module_size = 0;
    int fmt_dist = 16;
    int rs_cost = 100000;
    int version = 0;
    int qr_size = 0;
    Point2D alignments[QR_MAX_ALIGNMENTS];
    int alignment_count = 0;

    run_all_attempts(
      lpx,
      lw,
      lh,
      is_finest,
      &decoded,
      &fmt_dist,
      &rs_cost,
      &corners,
      finders,
      &module_size,
      &version,
      &qr_size,
      alignments,
      &alignment_count
    );

    /* Inverted-polarity retry at this level — handles light-on-dark QRs.
       Only fires if nothing at this level has decoded confidently.
       Confidence = fmt_dist <= 1 (format info BCH can correct 1-bit errors
       in its 15-bit code, so a 0-or-1-bit fmt_dist reliably indicates a
       real QR). Length is not a trust signal: legitimate v1 QRs routinely
       encode 2-4 characters. */
    if (!(decoded && fmt_dist <= 1)) {
      uint8_t *inv = invert_gray(lpx, lw, lh);
      if (inv) {
        run_all_attempts(
          inv,
          lw,
          lh,
          is_finest,
          &decoded,
          &fmt_dist,
          &rs_cost,
          &corners,
          finders,
          &module_size,
          &version,
          &qr_size,
          alignments,
          &alignment_count
        );
        free(inv);
      }
    }

    if (decoded) {
      scale_decode_state(
        &corners,
        finders,
        &module_size,
        alignments,
        alignment_count,
        scale
      );
      if (decode_is_better(
            best_decoded,
            best_fmt_dist,
            best_rs_cost,
            decoded,
            fmt_dist,
            rs_cost
          )) {
        free(best_decoded);
        best_decoded = decoded;
        best_fmt_dist = fmt_dist;
        best_rs_cost = rs_cost;
        best_corners = corners;
        for (int i = 0; i < 3; i++) {
          best_finders[i] = finders[i];
        }
        best_module_size = module_size;
        best_version = version;
        best_qr_size = qr_size;
        best_alignment_count = alignment_count;
        for (int i = 0; i < alignment_count; i++) {
          best_alignments[i] = alignments[i];
        }
      }
      else {
        free(decoded);
      }
    }

    if (best_decoded && best_fmt_dist <= 1) {
      break;
    }
  }

  for (int i = 1; i < n_levels; i++) {
    if (lv_owned[i]) {
      free((uint8_t *)lv_px[i]);
    }
  }

#undef QR_PYRAMID_MAX
#undef QR_PYRAMID_MIN_SHORT
#undef QR_PYRAMID_TARGET_SHORT

  if (best_decoded) {
    FCVQRCode *qr = &result.codes[result.count++];
    qr->text = best_decoded;
    qr->corners = best_corners;
    for (int i = 0; i < 3; i++) {
      qr->finders[i] = best_finders[i];
    }
    qr->module_size = best_module_size;
    qr->version = best_version;
    qr->qr_size = best_qr_size;
    qr->alignment_count = (size_t)best_alignment_count;
    if (best_alignment_count > 0) {
      qr->alignments = malloc((size_t)best_alignment_count * sizeof(Point2D));
      if (qr->alignments) {
        for (int i = 0; i < best_alignment_count; i++) {
          qr->alignments[i] = best_alignments[i];
        }
      }
      else {
        qr->alignment_count = 0;
      }
    }
    else {
      qr->alignments = NULL;
    }
  }

  return result;
}

void fcv_free_qr_result(FCVQRCodeResult result) {
  if (result.codes) {
    for (size_t i = 0; i < result.count; i++) {
      free(result.codes[i].text);
      free(result.codes[i].alignments);
    }
    free(result.codes);
  }
}
// File: src/rgba_to_grayscale.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "rgba_to_grayscale.h"
#else
#include "flatcv.h"
#endif

/**
 * Convert raw RGBA row-major top-to-bottom image data
 * to a single channel grayscale image data.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the pixel data.
 * @return Pointer to the single channel grayscale image data.
 */
uint8_t *fcv_rgba_to_grayscale(
  uint32_t width,
  uint32_t height,
  uint8_t const *const data
) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;

  uint8_t *grayscale_data = malloc(img_length_px);

  if (!grayscale_data) { // Memory allocation failed
    return NULL;
  }

  // Process each pixel row by row
  for (size_t i = 0; i < img_length_px; i++) {
    size_t rgba_index = i * 4;

    uint8_t r = data[rgba_index];
    uint8_t g = data[rgba_index + 1];
    uint8_t b = data[rgba_index + 2];

    uint8_t gray = (r * R_WEIGHT + g * G_WEIGHT + b * B_WEIGHT) >> 8;

    grayscale_data[i] = gray;
  }

  return grayscale_data;
}
// File: src/rotate.c
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#ifndef FLATCV_AMALGAMATION
#include "rotate.h"
#else
#include "flatcv.h"
#endif

/**
 * Rotate an image 90 degrees clockwise.
 */
uint8_t *
fcv_rotate_90_cw(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *rotated_data = malloc(img_length_byte);

  if (!rotated_data) {
    return NULL;
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      size_t src_index = ((size_t)y * width + x) * 4;
      size_t dst_index = ((size_t)x * height + (height - 1 - y)) * 4;

      rotated_data[dst_index] = data[src_index];
      rotated_data[dst_index + 1] = data[src_index + 1];
      rotated_data[dst_index + 2] = data[src_index + 2];
      rotated_data[dst_index + 3] = data[src_index + 3];
    }
  }

  return rotated_data;
}

/**
 * Rotate an image 180 degrees.
 */
uint8_t *
fcv_rotate_180(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *rotated_data = malloc(img_length_byte);

  if (!rotated_data) {
    return NULL;
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      size_t src_index = ((size_t)y * width + x) * 4;
      size_t dst_index =
        ((size_t)(height - 1 - y) * width + (width - 1 - x)) * 4;

      rotated_data[dst_index] = data[src_index];
      rotated_data[dst_index + 1] = data[src_index + 1];
      rotated_data[dst_index + 2] = data[src_index + 2];
      rotated_data[dst_index + 3] = data[src_index + 3];
    }
  }

  return rotated_data;
}

/**
 * Rotate an image 270 degrees clockwise (90 degrees counter-clockwise).
 */
uint8_t *
fcv_rotate_270_cw(uint32_t width, uint32_t height, uint8_t const *const data) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t num_pixels = (size_t)width * height;
  if (num_pixels > SIZE_MAX / 4) {
    return NULL;
  }
  size_t img_length_byte = num_pixels * 4;

  uint8_t *rotated_data = malloc(img_length_byte);

  if (!rotated_data) {
    return NULL;
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      size_t src_index = ((size_t)y * width + x) * 4;
      size_t dst_index = ((size_t)(width - 1 - x) * height + y) * 4;

      rotated_data[dst_index] = data[src_index];
      rotated_data[dst_index + 1] = data[src_index + 1];
      rotated_data[dst_index + 2] = data[src_index + 2];
      rotated_data[dst_index + 3] = data[src_index + 3];
    }
  }

  return rotated_data;
}
// File: src/single_to_multichannel.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "single_to_multichannel.h"
#else
#include "flatcv.h"
#endif

/**
 * Convert single channel grayscale image data to
 * RGBA row-major top-to-bottom image data.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param data Pointer to the pixel data.
 */
uint8_t *fcv_single_to_multichannel(
  uint32_t width,
  uint32_t height,
  uint8_t const *const data
) {
  if (!data || width == 0 || height == 0) {
    return NULL;
  }

  // Check for overflow: width * height * 4
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;
  if (img_length_px > SIZE_MAX / 4) {
    return NULL;
  }

  uint8_t *multichannel_data = malloc(img_length_px * 4);

  if (!multichannel_data) { // Memory allocation failed
    return NULL;
  }

  for (size_t i = 0; i < img_length_px; i++) {
    size_t rgba_index = i * 4;
    multichannel_data[rgba_index] = data[i];
    multichannel_data[rgba_index + 1] = data[i];
    multichannel_data[rgba_index + 2] = data[i];
    multichannel_data[rgba_index + 3] = 255;
  }

  return multichannel_data;
}
// File: src/sobel_edge_detection.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "conversion.h"
#include "perspectivetransform.h"
#include "rgba_to_grayscale.h"
#include "sobel_edge_detection.h"
#else
#include "flatcv.h"
#endif

/**
 * Apply Sobel edge detection to the image data and return single-channel
 * grayscale data. Uses Sobel kernels to detect edges in horizontal and vertical
 * directions, then combines them to get the edge magnitude.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param channels Number of channels in the input image.
 *                 (1: Grayscale, 3: RGB, 4: RGBA)
 * @param data Pointer to the input pixel data.
 * @return Pointer to the single-channel grayscale edge-detected image data.
 */
uint8_t *fcv_sobel_edge_detection(
  uint32_t width,
  uint32_t height,
  uint32_t channels,
  uint8_t const *const data
) {
  if (!data || width == 0 || height == 0 || channels == 0) {
    return NULL;
  }

  // Check for overflow: width * height
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;

  // Check for overflow in magnitudes allocation
  if (img_length_px > SIZE_MAX / sizeof(double)) {
    return NULL;
  }

  uint8_t *grayscale_data;
  bool allocated_grayscale = false;

  if (channels == 1) {
    // Single-channel input, use data directly
    grayscale_data = (uint8_t *)data;
    allocated_grayscale = false;
  }
  else {
    // Multi-channel input, convert to grayscale
    grayscale_data = fcv_rgba_to_grayscale(width, height, data);
    if (!grayscale_data) {
      return NULL;
    }
    allocated_grayscale = true;
  }

  uint8_t *sobel_data = malloc(img_length_px);
  if (!sobel_data) {
    if (allocated_grayscale) {
      free(grayscale_data);
    }
    return NULL;
  }

  // Sobel kernels
  int32_t sobel_x[3][3] = {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}};
  int32_t sobel_y[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}};

  // Temporary buffer to store magnitudes for normalization
  double *magnitudes = malloc(img_length_px * sizeof(double));
  if (!magnitudes) {
    if (allocated_grayscale) {
      free(grayscale_data);
    }
    free(sobel_data);
    return NULL;
  }

  double min_magnitude = INFINITY;
  double max_magnitude = 0.0;

  // First pass: calculate all magnitudes and find min/max
  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      int32_t gx = 0;
      int32_t gy = 0;

      // Apply Sobel kernels
      for (int32_t ky = -1; ky <= 1; ky++) {
        for (int32_t kx = -1; kx <= 1; kx++) {
          int32_t px = x + kx;
          int32_t py = y + ky;

          // Handle boundaries by using nearest pixel values
          if (px < 0) {
            px = 0;
          }
          if ((uint32_t)px >= width) {
            px = width - 1;
          }
          if (py < 0) {
            py = 0;
          }
          if ((uint32_t)py >= height) {
            py = height - 1;
          }

          uint8_t pixel = grayscale_data[(size_t)py * width + px];
          gx += pixel * sobel_x[ky + 1][kx + 1];
          gy += pixel * sobel_y[ky + 1][kx + 1];
        }
      }

      double magnitude = sqrt((double)gx * gx + (double)gy * gy);
      magnitudes[(size_t)y * width + x] = magnitude;

      if (magnitude < min_magnitude) {
        min_magnitude = magnitude;
      }
      if (magnitude > max_magnitude) {
        max_magnitude = magnitude;
      }
    }
  }

  // Second pass: normalize and assign to output
  double range = max_magnitude - min_magnitude;
  if (range == 0.0) {
    range = 1.0; // Avoid division by zero
  }

  for (uint32_t y = 0; y < height; y++) {
    for (uint32_t x = 0; x < width; x++) {
      double magnitude = magnitudes[(size_t)y * width + x];

      // Normalize to 0-255 range based on actual min/max
      int32_t final_magnitude =
        (int32_t)(((magnitude - min_magnitude) / range) * 255.0);

      // Clamp to valid range
      if (final_magnitude < 0) {
        final_magnitude = 0;
      }
      if (final_magnitude > 255) {
        final_magnitude = 255;
      }

      sobel_data[(size_t)y * width + x] = (uint8_t)final_magnitude;
    }
  }

  free(magnitudes);

  if (allocated_grayscale) {
    free(grayscale_data);
  }
  return sobel_data;
}
// File: src/sort_corners.c
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#else
#include "flatcv.h"
#endif

typedef struct {
  double r;     // radius
  double theta; // angle
} PolarPoint;

// Helper function to convert Cartesian to polar coordinates
PolarPoint cartesian_to_polar(Point2D cart) {
  PolarPoint polar;
  polar.r = sqrt(cart.x * cart.x + cart.y * cart.y);
  double theta = atan2(cart.y, cart.x);

  // Normalize theta: Convert to degrees and apply normalization
  double theta_deg = theta * 180.0 / M_PI;
  if (theta_deg < 0) {
    polar.theta = -theta_deg;
  }
  else {
    polar.theta = -(theta_deg - 360.0);
  }

  return polar;
}

// Comparison function for qsort (sorting by angle in descending order)
int compare_polar_angles(const void *a, const void *b) {
  const PolarPoint *pa = (const PolarPoint *)a;
  const PolarPoint *pb = (const PolarPoint *)b;

  if (pa->theta < pb->theta) {
    return 1; // Descending order
  }
  if (pa->theta > pb->theta) {
    return -1;
  }
  return 0;
}

// Helper structure to keep track of original indices during sorting
typedef struct {
  PolarPoint polar;
  uint32_t original_index;
} IndexedPolarPoint;

int compare_indexed_polar_angles(const void *a, const void *b) {
  const IndexedPolarPoint *pa = (const IndexedPolarPoint *)a;
  const IndexedPolarPoint *pb = (const IndexedPolarPoint *)b;

  if (pa->polar.theta < pb->polar.theta) {
    return 1; // Descending order
  }
  if (pa->polar.theta > pb->polar.theta) {
    return -1;
  }
  return 0;
}

Corners sort_corners(
  uint32_t width,
  uint32_t height,
  uint32_t out_width,
  uint32_t out_height,
  Point2D *corners,
  uint32_t num_corners,
  Point2D *result
) {
  if (num_corners < 3) {
    // Not enough corners, return zeros
    Corners empty_corners = {0, 0, 0, 0, 0, 0, 0, 0};
    return empty_corners;
  }

  // Allocate memory for working corners (max of num_corners or 4 for 3-corner
  // case)
  uint32_t max_corners = (num_corners >= 4) ? num_corners : 4;
  Point2D *working_corners = malloc(max_corners * sizeof(Point2D));
  if (!working_corners) {
    Corners empty_corners = {0, 0, 0, 0, 0, 0, 0, 0};
    return empty_corners;
  }

  uint32_t corners_to_process;

  if (num_corners == 3) {
    // Copy the 3 corners we have
    working_corners[0] = corners[0];
    working_corners[1] = corners[1];
    working_corners[2] = corners[2];

    // Calculate the fourth corner to complete a parallelogram
    // For 3 corners A, B, C, we need to determine which corner is missing
    // and calculate it using vector addition

    // First, sort the 3 corners by position to understand the layout
    Point2D sorted[3] = {corners[0], corners[1], corners[2]};

    // Find bounding box of the 3 corners
    double min_x = sorted[0].x, max_x = sorted[0].x;
    double min_y = sorted[0].y, max_y = sorted[0].y;
    for (int i = 1; i < 3; i++) {
      if (sorted[i].x < min_x) {
        min_x = sorted[i].x;
      }
      if (sorted[i].x > max_x) {
        max_x = sorted[i].x;
      }
      if (sorted[i].y < min_y) {
        min_y = sorted[i].y;
      }
      if (sorted[i].y > max_y) {
        max_y = sorted[i].y;
      }
    }

    // Determine which corner is missing by finding which position in the
    // rectangle is empty
    bool has_tl = false, has_tr = false, has_bl = false, has_br = false;
    for (int i = 0; i < 3; i++) {
      double x = sorted[i].x;
      double y = sorted[i].y;

      // Use small tolerance for floating point comparison
      double tolerance = 1.0;

      if (fabs(x - min_x) < tolerance && fabs(y - min_y) < tolerance) {
        has_tl = true;
      }
      else if (fabs(x - max_x) < tolerance && fabs(y - min_y) < tolerance) {
        has_tr = true;
      }
      else if (fabs(x - min_x) < tolerance && fabs(y - max_y) < tolerance) {
        has_bl = true;
      }
      else if (fabs(x - max_x) < tolerance && fabs(y - max_y) < tolerance) {
        has_br = true;
      }
    }

    // Calculate the missing corner
    Point2D missing_corner;
    if (!has_tl) {
      missing_corner.x = min_x;
      missing_corner.y = min_y;
    }
    else if (!has_tr) {
      missing_corner.x = max_x;
      missing_corner.y = min_y;
    }
    else if (!has_bl) {
      missing_corner.x = min_x;
      missing_corner.y = max_y;
    }
    else if (!has_br) {
      missing_corner.x = max_x;
      missing_corner.y = max_y;
    }
    else {
      // Fallback: use parallelogram completion
      // For a parallelogram ABCD where we have A, B, C and need D:
      // D = A + C - B (assuming B and D are diagonal)
      // Try all combinations and pick the best one
      Point2D candidates[3];
      candidates[0].x = corners[0].x + corners[1].x - corners[2].x;
      candidates[0].y = corners[0].y + corners[1].y - corners[2].y;
      candidates[1].x = corners[0].x + corners[2].x - corners[1].x;
      candidates[1].y = corners[0].y + corners[2].y - corners[1].y;
      candidates[2].x = corners[1].x + corners[2].x - corners[0].x;
      candidates[2].y = corners[1].y + corners[2].y - corners[0].y;

      // Choose the candidate that gives the most reasonable result
      missing_corner = candidates[2]; // Default to the last one
      double best_score = -1;

      for (int i = 0; i < 3; i++) {
        double score = 0;
        if (candidates[i].x >= 0) {
          score += 1;
        }
        if (candidates[i].y >= 0) {
          score += 1;
        }

        // Check if it's not a duplicate
        bool is_duplicate = false;
        for (int j = 0; j < 3; j++) {
          if (fabs(candidates[i].x - corners[j].x) < 0.1 &&
              fabs(candidates[i].y - corners[j].y) < 0.1) {
            is_duplicate = true;
            break;
          }
        }
        if (!is_duplicate) {
          score += 5;
        }

        if (score > best_score) {
          best_score = score;
          missing_corner = candidates[i];
        }
      }
    }

    working_corners[3] = missing_corner;
    corners_to_process = 4;
  }
  else {
    // Copy all existing corners
    for (uint32_t i = 0; i < num_corners; i++) {
      working_corners[i] = corners[i];
    }
    corners_to_process = num_corners;
  }

  // Clockwise corner sorting starting from top-left corner
  // Works with any number of corners >= 3

  // Step 1: Find the center point (centroid)
  double center_x = 0.0, center_y = 0.0;
  for (uint32_t i = 0; i < corners_to_process; i++) {
    center_x += working_corners[i].x;
    center_y += working_corners[i].y;
  }
  center_x /= corners_to_process;
  center_y /= corners_to_process;

  // Step 2: Find top-left in document-relative coordinates. A plain x+y
  // score lets the long axis dominate on skewed receipts, choosing bottom-left
  // (or top-right) instead. Normalize by the document bounds, not image bounds,
  // so the labels also survive the detector's non-uniform resize to 256x256.
  double min_x = working_corners[0].x, max_x = min_x;
  double min_y = working_corners[0].y, max_y = min_y;
  for (uint32_t i = 1; i < corners_to_process; i++) {
    min_x = fmin(min_x, working_corners[i].x);
    max_x = fmax(max_x, working_corners[i].x);
    min_y = fmin(min_y, working_corners[i].y);
    max_y = fmax(max_y, working_corners[i].y);
  }
  double span_x = max_x > min_x ? max_x - min_x : 1.0;
  double span_y = max_y > min_y ? max_y - min_y : 1.0;
  uint32_t top_left_idx = 0;
  double min_sum = (working_corners[0].x - min_x) / span_x +
                   (working_corners[0].y - min_y) / span_y;
  for (uint32_t i = 1; i < corners_to_process; i++) {
    double sum = (working_corners[i].x - min_x) / span_x +
                 (working_corners[i].y - min_y) / span_y;
    if (sum < min_sum) {
      min_sum = sum;
      top_left_idx = i;
    }
  }

  // Step 3: Create array with indices and calculate angles relative to center
  typedef struct {
    uint32_t index;
    double angle;
    double distance;
  } CornerInfo;

  CornerInfo *corner_info = malloc(corners_to_process * sizeof(CornerInfo));
  if (!corner_info) {
    free(working_corners);
    Corners empty_corners = {0, 0, 0, 0, 0, 0, 0, 0};
    return empty_corners;
  }

  for (uint32_t i = 0; i < corners_to_process; i++) {
    corner_info[i].index = i;
    corner_info[i].distance = sqrt(
      (working_corners[i].x - center_x) * (working_corners[i].x - center_x) +
      (working_corners[i].y - center_y) * (working_corners[i].y - center_y)
    );

    // Calculate angle from center to corner
    double dx = working_corners[i].x - center_x;
    double dy = working_corners[i].y - center_y;
    corner_info[i].angle = atan2(dy, dx);

    // Normalize angle to [0, 2π) range
    if (corner_info[i].angle < 0) {
      corner_info[i].angle += 2.0 * M_PI;
    }
  }

  // Step 4: Calculate reference angle from center to top-left corner
  double top_left_angle = corner_info[top_left_idx].angle;

  // Step 5: Adjust all angles relative to top-left corner and sort clockwise
  for (uint32_t i = 0; i < corners_to_process; i++) {
    // Adjust angle relative to top-left corner
    corner_info[i].angle = corner_info[i].angle - top_left_angle;

    // Normalize to [0, 2π) range
    while (corner_info[i].angle < 0) {
      corner_info[i].angle += 2.0 * M_PI;
    }
    while (corner_info[i].angle >= 2.0 * M_PI) {
      corner_info[i].angle -= 2.0 * M_PI;
    }
  }

  // Step 6: Sort corners by angle (clockwise from top-left)
  for (uint32_t i = 0; i < corners_to_process - 1; i++) {
    for (uint32_t j = 0; j < corners_to_process - 1 - i; j++) {
      bool should_swap = false;

      if (corner_info[j].angle > corner_info[j + 1].angle) {
        should_swap = true;
      }
      else if (fabs(corner_info[j].angle - corner_info[j + 1].angle) < 1e-6) {
        // If angles are very close, sort by distance from center (closer first)
        if (corner_info[j].distance > corner_info[j + 1].distance) {
          should_swap = true;
        }
      }

      if (should_swap) {
        CornerInfo temp = corner_info[j];
        corner_info[j] = corner_info[j + 1];
        corner_info[j + 1] = temp;
      }
    }
  }

  // Step 7: Copy sorted corners to result
  for (uint32_t i = 0; i < corners_to_process; i++) {
    result[i] = working_corners[corner_info[i].index];
  }

  // Scale corners back to original image dimensions
  double scale_x = (double)width / out_width;
  double scale_y = (double)height / out_height;

  // Return corners struct using the sorted result
  Corners sorted_corners_result = {
    .tl_x = result[0].x * scale_x,
    .tl_y = result[0].y * scale_y,
    .tr_x = result[1].x * scale_x,
    .tr_y = result[1].y * scale_y,
    .br_x = result[2].x * scale_x,
    .br_y = result[2].y * scale_y,
    .bl_x = result[3].x * scale_x,
    .bl_y = result[3].y * scale_y
  };

  // Clean up allocated memory
  free(corner_info);
  free(working_corners);

  return sorted_corners_result;
}
// File: src/trim.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "crop.h"
#include "trim.h"
#else
#include "flatcv.h"
#endif

static bool
pixels_match(const uint8_t *pixel1, const uint8_t *pixel2, uint32_t channels) {
  for (uint32_t c = 0; c < channels; c++) {
    if (pixel1[c] != pixel2[c]) {
      return false;
    }
  }
  return true;
}

/**
 * Check if two pixels match within a given tolerance.
 *
 * @param pixel1 First pixel data.
 * @param pixel2 Second pixel data.
 * @param channels Number of channels.
 * @param tolerance Maximum allowed difference per channel (0-255).
 * @return true if all channels are within tolerance, false otherwise.
 */
static bool pixels_match_threshold(
  const uint8_t *pixel1,
  const uint8_t *pixel2,
  uint32_t channels,
  uint8_t tolerance
) {
  for (uint32_t c = 0; c < channels; c++) {
    int32_t diff = (int32_t)pixel1[c] - (int32_t)pixel2[c];
    if (diff < 0) {
      diff = -diff;
    }
    if ((uint8_t)diff > tolerance) {
      return false;
    }
  }
  return true;
}

/**
 * Trim border pixels that have the same color.
 *
 * @param width Pointer to width of the image (will be updated).
 * @param height Pointer to height of the image (will be updated).
 * @param channels Number of channels in the image.
 * @param data Pointer to the pixel data.
 * @return Pointer to the new trimmed image data.
 */
uint8_t *fcv_trim(
  int32_t *width,
  int32_t *height,
  uint32_t channels,
  uint8_t const *const data
) {
  if (!data || !width || !height || *width <= 0 || *height <= 0 ||
      channels == 0) {
    return NULL;
  }

  uint32_t w = (uint32_t)*width;
  uint32_t h = (uint32_t)*height;

  // Check for potential overflow in pixel index calculations
  if (w > SIZE_MAX / h || (size_t)w * h > SIZE_MAX / channels) {
    return NULL;
  }
  uint32_t left = 0;
  uint32_t top = 0;
  uint32_t right = w;
  uint32_t bottom = h;

  // Trim from the left
  while (left < right) {
    const uint8_t *ref_pixel = &data[(top * w + left) * channels];
    bool can_trim = true;

    // Check if the entire left column has the same color as the reference pixel
    for (uint32_t y = top; y < bottom; y++) {
      const uint8_t *pixel = &data[(y * w + left) * channels];
      if (!pixels_match(pixel, ref_pixel, channels)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim && right - left > 1) {
      left++;
    }
    else {
      break;
    }
  }

  // Trim from the right
  while (left < right) {
    const uint8_t *ref_pixel = &data[(top * w + (right - 1)) * channels];
    bool can_trim = true;

    // Check if the entire right column has the same color as the reference
    // pixel
    for (uint32_t y = top; y < bottom; y++) {
      const uint8_t *pixel = &data[(y * w + (right - 1)) * channels];
      if (!pixels_match(pixel, ref_pixel, channels)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim && right - left > 1) {
      right--;
    }
    else {
      break;
    }
  }

  // Trim from the top
  while (top < bottom && bottom - top > 1) {
    const uint8_t *ref_pixel = &data[(top * w + left) * channels];
    bool can_trim = true;

    // Check if the entire top row has the same color as the reference pixel
    for (uint32_t x = left; x < right; x++) {
      const uint8_t *pixel = &data[(top * w + x) * channels];
      if (!pixels_match(pixel, ref_pixel, channels)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim) {
      top++;
    }
    else {
      break;
    }
  }

  // Trim from the bottom
  while (top < bottom && bottom - top > 1) {
    const uint8_t *ref_pixel = &data[((bottom - 1) * w + left) * channels];
    bool can_trim = true;

    // Check if the entire bottom row has the same color as the reference pixel
    for (uint32_t x = left; x < right; x++) {
      const uint8_t *pixel = &data[((bottom - 1) * w + x) * channels];
      if (!pixels_match(pixel, ref_pixel, channels)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim) {
      bottom--;
    }
    else {
      break;
    }
  }

  // Calculate new dimensions
  uint32_t new_width = right - left;
  uint32_t new_height = bottom - top;

  // If no trimming was done, return a copy
  if (left == 0 && top == 0 && right == w && bottom == h) {
    size_t alloc_size = (size_t)w * h * channels;
    uint8_t *result = malloc(alloc_size);
    if (result) {
      memcpy(result, data, alloc_size);
    }
    return result;
  }

  // Use existing crop function to extract the trimmed region
  uint8_t *trimmed_data =
    fcv_crop(w, h, channels, data, left, top, new_width, new_height);

  // Update dimensions
  *width = (int32_t)new_width;
  *height = (int32_t)new_height;

  return trimmed_data;
}

/**
 * Trim border pixels that are within a threshold of the reference color.
 * This is useful for images with JPEG artifacts or slight vignetting.
 *
 * @param width Pointer to width of the image (will be updated).
 * @param height Pointer to height of the image (will be updated).
 * @param channels Number of channels in the image.
 * @param data Pointer to the pixel data.
 * @param threshold_percent Tolerance percentage (0-100). A value of 2 means
 *        pixels within 2% (≈5 units) of the reference color will be trimmed.
 * @return Pointer to the new trimmed image data.
 */
uint8_t *fcv_trim_threshold(
  int32_t *width,
  int32_t *height,
  uint32_t channels,
  uint8_t const *const data,
  double threshold_percent
) {
  if (!data || !width || !height || *width <= 0 || *height <= 0 ||
      channels == 0) {
    return NULL;
  }

  // Validate threshold_percent - reject invalid values
  if (!isfinite(threshold_percent)) {
    return NULL;
  }

  // Clamp threshold to valid range and convert to absolute value (0-255)
  if (threshold_percent < 0.0) {
    threshold_percent = 0.0;
  }
  if (threshold_percent > 100.0) {
    threshold_percent = 100.0;
  }
  uint8_t tolerance = (uint8_t)(threshold_percent * 255.0 / 100.0);

  // If tolerance is 0, use the exact match version
  if (tolerance == 0) {
    return fcv_trim(width, height, channels, data);
  }

  uint32_t w = (uint32_t)*width;
  uint32_t h = (uint32_t)*height;

  // Check for potential overflow in pixel index calculations
  if (w > SIZE_MAX / h || (size_t)w * h > SIZE_MAX / channels) {
    return NULL;
  }
  uint32_t left = 0;
  uint32_t top = 0;
  uint32_t right = w;
  uint32_t bottom = h;

  // Trim from the left
  while (left < right) {
    const uint8_t *ref_pixel = &data[(top * w + left) * channels];
    bool can_trim = true;

    // Check if the entire left column matches within threshold
    for (uint32_t y = top; y < bottom; y++) {
      const uint8_t *pixel = &data[(y * w + left) * channels];
      if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim && right - left > 1) {
      left++;
    }
    else {
      break;
    }
  }

  // Trim from the right
  while (left < right) {
    const uint8_t *ref_pixel = &data[(top * w + (right - 1)) * channels];
    bool can_trim = true;

    // Check if the entire right column matches within threshold
    for (uint32_t y = top; y < bottom; y++) {
      const uint8_t *pixel = &data[(y * w + (right - 1)) * channels];
      if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim && right - left > 1) {
      right--;
    }
    else {
      break;
    }
  }

  // Trim from the top
  while (top < bottom && bottom - top > 1) {
    const uint8_t *ref_pixel = &data[(top * w + left) * channels];
    bool can_trim = true;

    // Check if the entire top row matches within threshold
    for (uint32_t x = left; x < right; x++) {
      const uint8_t *pixel = &data[(top * w + x) * channels];
      if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim) {
      top++;
    }
    else {
      break;
    }
  }

  // Trim from the bottom
  while (top < bottom && bottom - top > 1) {
    const uint8_t *ref_pixel = &data[((bottom - 1) * w + left) * channels];
    bool can_trim = true;

    // Check if the entire bottom row matches within threshold
    for (uint32_t x = left; x < right; x++) {
      const uint8_t *pixel = &data[((bottom - 1) * w + x) * channels];
      if (!pixels_match_threshold(pixel, ref_pixel, channels, tolerance)) {
        can_trim = false;
        break;
      }
    }

    if (can_trim) {
      bottom--;
    }
    else {
      break;
    }
  }

  // Calculate new dimensions
  uint32_t new_width = right - left;
  uint32_t new_height = bottom - top;

  // If no trimming was done, return a copy
  if (left == 0 && top == 0 && right == w && bottom == h) {
    size_t alloc_size = (size_t)w * h * channels;
    uint8_t *result = malloc(alloc_size);
    if (result) {
      memcpy(result, data, alloc_size);
    }
    return result;
  }

  // Use existing crop function to extract the trimmed region
  uint8_t *trimmed_data =
    fcv_crop(w, h, channels, data, left, top, new_width, new_height);

  // Update dimensions
  *width = (int32_t)new_width;
  *height = (int32_t)new_height;

  return trimmed_data;
}
// File: src/watershed_segmentation.c
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#ifndef FLATCV_AMALGAMATION
#include "1_types.h"
#include "watershed_segmentation.h"
#else
#include "flatcv.h"
#endif

typedef struct {
  int32_t x, y;
  int32_t label;
} QueueItem;

typedef struct {
  QueueItem *items;
  int32_t front;
  int32_t rear;
  int32_t size;
  int32_t capacity;
} Queue;

static Queue *create_queue(size_t capacity) {
  if (capacity == 0 || capacity > SIZE_MAX / sizeof(QueueItem) ||
      capacity > INT32_MAX) {
    return NULL;
  }

  Queue *q = malloc(sizeof(Queue));
  if (!q) {
    return NULL;
  }

  q->items = malloc(capacity * sizeof(QueueItem));
  if (!q->items) {
    free(q);
    return NULL;
  }

  q->front = 0;
  q->rear = -1;
  q->size = 0;
  q->capacity = (int32_t)capacity;
  return q;
}

static void destroy_queue(Queue *q) {
  if (q) {
    free(q->items);
    free(q);
  }
}

static int32_t is_queue_empty(Queue *q) { return q->size == 0; }

static int32_t enqueue(Queue *q, int32_t x, int32_t y, int32_t label) {
  if (q->size >= q->capacity) {
    return 0;
  }

  q->rear = (q->rear + 1) % q->capacity;
  q->items[q->rear].x = x;
  q->items[q->rear].y = y;
  q->items[q->rear].label = label;
  q->size++;
  return 1;
}

static QueueItem dequeue(Queue *q) {
  QueueItem item = {-1, -1, -1};
  if (q->size == 0) {
    return item;
  }

  item = q->items[q->front];
  q->front = (q->front + 1) % q->capacity;
  q->size--;
  return item;
}

/**
 * Watershed segmentation
 * using (x, y) coordinate markers with elevation-based flooding
 *
 * Implements the watershed transform for image segmentation by treating the
 * grayscale image as an elevation map. Water floods from the marker points,
 * and watershed lines form where different regions would meet. Lower
 * intensity values represent valleys where water accumulates, and higher
 * values represent hills/ridges.
 *
 * @param width Width of the image.
 * @param height Height of the image.
 * @param grayscale_data Pointer to the input grayscale image data.
 * @param markers Array of Point2D markers with x,y coordinates.
 * @param num_markers Number of markers in the array.
 * @return Pointer to the segmented image data.
 */
uint8_t *fcv_watershed_segmentation(
  uint32_t width,
  uint32_t height,
  uint8_t const *const grayscale_data,
  Point2D *markers,
  uint32_t num_markers,
  bool create_boundaries
) {
  if (!grayscale_data || !markers || num_markers == 0) {
    return NULL;
  }

  if (width == 0 || height == 0) {
    return NULL;
  }

  // Validate all marker positions and coordinates
  for (uint32_t m = 0; m < num_markers; m++) {
    // Check for NaN/Inf
    if (!isfinite(markers[m].x) || !isfinite(markers[m].y)) {
      return NULL;
    }

    int32_t marker_x = (int32_t)markers[m].x;
    int32_t marker_y = (int32_t)markers[m].y;

    // Check bounds - if any marker is invalid, fail the entire operation
    if (marker_x < 0 || marker_x >= (int32_t)width || marker_y < 0 ||
        marker_y >= (int32_t)height) {
      return NULL;
    }
  }

  // Check for overflow: width * height
  if (width > SIZE_MAX / height) {
    return NULL;
  }
  size_t img_length_px = (size_t)width * height;

  // Check for overflow in output_data allocation (pixels * 4)
  if (img_length_px > SIZE_MAX / 4) {
    return NULL;
  }

  // Check for overflow in labels allocation
  if (img_length_px > SIZE_MAX / sizeof(int32_t)) {
    return NULL;
  }

  // Create output data
  uint8_t *output_data = malloc(img_length_px * 4);
  if (!output_data) {
    return NULL;
  }

  // Initialize labels array (-1 = unvisited, 0+ = region labels)
  int32_t *labels = malloc(img_length_px * sizeof(int32_t));
  if (!labels) {
    free(output_data);
    return NULL;
  }

  // Initialize all pixels as unvisited
  for (uint32_t i = 0; i < img_length_px; i++) {
    labels[i] = -1;
  }

  // Create queue for flood fill
  Queue *queue = create_queue(img_length_px);
  if (!queue) {
    free(output_data);
    free(labels);
    return NULL;
  }

  // Initialize markers and add to queue
  for (uint32_t m = 0; m < num_markers; m++) {
    int32_t marker_x = (int32_t)markers[m].x;
    int32_t marker_y = (int32_t)markers[m].y;
    int32_t idx = marker_y * width + marker_x;
    int32_t label = (int32_t)m; // Label starts from 0
    labels[idx] = label;
    enqueue(queue, marker_x, marker_y, label);
  }

  // Level-wise watershed algorithm - process pixels in elevation order
  int32_t dx[] = {-1, 1, 0, 0};
  int32_t dy[] = {0, 0, -1, 1};

  // Process each elevation level from 0 to 255
  for (int32_t current_level = 0; current_level <= 255; current_level++) {
    // Single-step expansion: each region grows by exactly one pixel layer per
    // iteration
    bool changed = true;
    while (changed) {
      changed = false;

      // Find all pixels at current level that should be added in this step
      for (uint32_t y = 0; y < height; y++) {
        for (uint32_t x = 0; x < width; x++) {
          int32_t idx = y * width + x;

          // Skip if already labeled
          if (labels[idx] != -1) {
            continue;
          }

          // Allow water to flow downhill:
          // Process pixels at or below current level.
          // This enables proper watershed behavior
          // where water flows from high to low elevation.
          bool can_flood = (grayscale_data[idx] <= current_level);

          if (!can_flood) {
            continue;
          }

          // Check if this pixel neighbors any labeled pixel
          int32_t neighbor_label = -1;
          bool multiple_labels = false;

          for (int32_t d = 0; d < 4; d++) {
            int32_t nx = x + dx[d];
            int32_t ny = y + dy[d];

            if (nx >= 0 && nx < (int32_t)width && ny >= 0 &&
                ny < (int32_t)height) {
              int32_t neighbor_idx = ny * width + nx;
              if (labels[neighbor_idx] != -1) {
                if (neighbor_label == -1) {
                  neighbor_label = labels[neighbor_idx];
                }
                else if (neighbor_label != labels[neighbor_idx]) {
                  multiple_labels = true;
                  break;
                }
              }
            }
          }

          // If found exactly one labeled neighbor, mark for expansion
          // If multiple labels and boundaries disabled, use first found label
          if (neighbor_label != -1 &&
              (!multiple_labels || !create_boundaries)) {
            enqueue(queue, x, y, neighbor_label);
            changed = true;
          }
        }
      }

      // Apply all expansions from this step simultaneously
      while (!is_queue_empty(queue)) {
        QueueItem item = dequeue(queue);
        int32_t idx = item.y * width + item.x;
        labels[idx] = item.label;
      }
    }
  }

  destroy_queue(queue);

  // Generate distinct colors for each region
  uint8_t colors[10][3] = {
    {255, 0, 0},     // red
    {0, 255, 0},     // green
    {0, 0, 255},     // blue
    {255, 255, 0},   // yellow
    {255, 0, 255},   // magenta
    {0, 255, 255},   // cyan
    {255, 128, 0},   // orange
    {128, 0, 255},   // purple
    {255, 192, 203}, // pink
    {128, 128, 128}  // gray
  };

  // Create output image
  for (uint32_t i = 0; i < img_length_px; i++) {
    uint32_t rgba_idx = i * 4;
    int32_t label = labels[i];

    if (label == -1) {
      // Unassigned pixels -> background (black)
      output_data[rgba_idx] = 0;     // R
      output_data[rgba_idx + 1] = 0; // G
      output_data[rgba_idx + 2] = 0; // B
    }
    else if (label >= 0 && label < 10) {
      // Assigned to a region
      output_data[rgba_idx] = colors[label][0];     // R
      output_data[rgba_idx + 1] = colors[label][1]; // G
      output_data[rgba_idx + 2] = colors[label][2]; // B
    }
    else {
      // Fallback for more than 10 regions
      output_data[rgba_idx] = 128;     // R
      output_data[rgba_idx + 1] = 128; // G
      output_data[rgba_idx + 2] = 128; // B
    }
    output_data[rgba_idx + 3] = 255; // A
  }

  free(labels);

  return output_data;
}
/* End of FlatCV amalgamation */