///|
/// Nearest-neighbor resampling.
fn resize_nearest(img : Image, dst_h : Int, dst_w : Int) -> Image {
let out = Image::new(dst_h, dst_w)
if img.is_empty() || dst_h == 0 || dst_w == 0 {
return out
}
for y = 0; y < dst_h; y = y + 1 {
let sy = clampi(y * img.h / dst_h, 0, img.h - 1)
for x = 0; x < dst_w; x = x + 1 {
let sx = clampi(x * img.w / dst_w, 0, img.w - 1)
copy_px(img, img.offset(sy, sx), out, out.offset(y, x))
}
}
out
}
///|
/// Bilinear filter weight: 1 - |x| for |x| < 1, else 0.
fn bilinear_filter(x : Double) -> Double {
let ax = if x < 0.0 { -x } else { x }
if ax < 1.0 {
1.0 - ax
} else {
0.0
}
}
///|
/// Precompute bilinear weights for one axis.
fn precompute_bilinear_weights(
src_size : Int,
dst_size : Int,
) -> Array[(Array[Double], Int, Int)] {
let scale = src_size.to_double() / dst_size.to_double()
let support = if scale < 1.0 { 1.0 } else { scale }
let inv_fs = 1.0 / support
let result = Array::make(dst_size, ([], 0, 0))
for i = 0; i < dst_size; i = i + 1 {
let center = (i.to_double() + 0.5) * scale
let xmin = (center - support + 0.5).floor().to_int().max(0)
let xmax = (center + support + 0.5).ceil().to_int().min(src_size)
let n = xmax - xmin
let weights = Array::make(n, 0.0)
let mut ww = 0.0
for j = 0; j < n; j = j + 1 {
let w = bilinear_filter(((xmin + j).to_double() - center + 0.5) * inv_fs)
weights[j] = w
ww = ww + w
}
if ww > 0.0 {
for j = 0; j < n; j = j + 1 {
weights[j] = weights[j] / ww
}
}
result[i] = (weights, xmin, xmax)
}
result
}
///|
/// Bilinear resampling (separable, matches Pillow).
fn resize_bilinear(img : Image, dst_h : Int, dst_w : Int) -> Image {
let out = Image::new(dst_h, dst_w)
if img.is_empty() || dst_h == 0 || dst_w == 0 {
return out
}
let h = img.h
let w = img.w
// Horizontal pass: src (h, w, 4) -> temp (h, dst_w, 4)
let x_weights = precompute_bilinear_weights(w, dst_w)
let temp = Array::make(h * dst_w * 4, 0.0)
for y = 0; y < h; y = y + 1 {
for dx = 0; dx < dst_w; dx = dx + 1 {
let (wts, xmin, xmax) = x_weights[dx]
for c = 0; c < 4; c = c + 1 {
let mut acc = 0.0
for j = 0; j < xmax - xmin; j = j + 1 {
let sx = xmin + j
acc = acc + wts[j] * img.data[(y * w + sx) * 4 + c].to_double()
}
temp[(y * dst_w + dx) * 4 + c] = acc
}
}
}
// Vertical pass: temp (h, dst_w, 4) -> final (dst_h, dst_w, 4)
let y_weights = precompute_bilinear_weights(h, dst_h)
for dy = 0; dy < dst_h; dy = dy + 1 {
let (wts, ymin, ymax) = y_weights[dy]
for x = 0; x < dst_w; x = x + 1 {
for c = 0; c < 4; c = c + 1 {
let mut acc = 0.0
for j = 0; j < ymax - ymin; j = j + 1 {
let sy = ymin + j
acc = acc + wts[j] * temp[(sy * dst_w + x) * 4 + c]
}
out.data[(dy * dst_w + x) * 4 + c] = round_byte(acc)
}
}
}
out
}
///|
/// Cubic kernel for bicubic interpolation (a = -0.5).
fn cubic_w(t : Double) -> Double {
let a = -0.5
let at = if t < 0.0 { -t } else { t }
if at <= 1.0 {
(a + 2.0) * at * at * at - (a + 3.0) * at * at + 1.0
} else if at < 2.0 {
a * at * at * at - 5.0 * a * at * at + 8.0 * a * at - 4.0 * a
} else {
0.0
}
}
///|
/// Bicubic resampling using a 4x4 neighborhood with the standard cubic kernel.
fn resize_bicubic(img : Image, dst_h : Int, dst_w : Int) -> Image {
let out = Image::new(dst_h, dst_w)
if img.is_empty() || dst_h == 0 || dst_w == 0 {
return out
}
let sh = img.h.to_double()
let sw = img.w.to_double()
for y = 0; y < dst_h; y = y + 1 {
let fy = clampd(
(y.to_double() + 0.5) * sh / dst_h.to_double() - 0.5,
0.0,
sh - 1.0,
)
let y0 = @math.floor(fy).to_int()
let wy = fy - y0.to_double()
for x = 0; x < dst_w; x = x + 1 {
let fx = clampd(
(x.to_double() + 0.5) * sw / dst_w.to_double() - 0.5,
0.0,
sw - 1.0,
)
let x0 = @math.floor(fx).to_int()
let wx = fx - x0.to_double()
let oo = out.offset(y, x)
for c = 0; c < 4; c = c + 1 {
let mut acc = 0.0
for j = -1; j <= 2; j = j + 1 {
let wyi = cubic_w(wy - j.to_double())
let sy = clampi(y0 + j, 0, img.h - 1)
for i = -1; i <= 2; i = i + 1 {
let wxi = cubic_w(wx - i.to_double())
let sx = clampi(x0 + i, 0, img.w - 1)
acc = acc + wyi * wxi * img.data[img.offset(sy, sx) + c].to_double()
}
}
out.data[oo + c] = round_byte(acc)
}
}
}
out
}
///|
/// Resize the image to `(dst_h, dst_w)` using the given interpolation.
///
/// - `dst_h`, `dst_w`: target dimensions in pixels.
/// - `interp`: resampling method (`Nearest`, `Bilinear`, or `Bicubic`).
pub fn resize(img : Image, dst_h : Int, dst_w : Int, interp : Interp) -> Image {
match interp {
Nearest => resize_nearest(img, dst_h, dst_w)
Bilinear => resize_bilinear(img, dst_h, dst_w)
Bicubic => resize_bicubic(img, dst_h, dst_w)
}
}
///|
/// Scale both dimensions by a uniform factor.
///
/// - `scale`: scaling factor (e.g. `0.5` halves, `2.0` doubles).
/// - `interp`: resampling method (`Nearest`, `Bilinear`, or `Bicubic`).
///
/// Output dimensions are `round(h * scale) x round(w * scale)`.
pub fn rescale(img : Image, scale : Double, interp : Interp) -> Image {
let dh = @math.round(img.h.to_double() * scale).to_int()
let dw = @math.round(img.w.to_double() * scale).to_int()
resize(img, dh, dw, interp)
}
///|
/// Scale to fit inside `max_h x max_w`, preserving aspect ratio.
///
/// - `max_h`, `max_w`: maximum bounding dimensions.
/// - `interp`: resampling method (`Nearest`, `Bilinear`, or `Bicubic`).
///
/// The result fits entirely within the box and at least one dimension equals
/// its bound. Returns a clone if the image is empty.
pub fn resize_to_fit(
img : Image,
max_h : Int,
max_w : Int,
interp : Interp,
) -> Image {
if img.is_empty() {
return img.clone()
}
let sy = max_h.to_double() / img.h.to_double()
let sx = max_w.to_double() / img.w.to_double()
let s = if sy < sx { sy } else { sx }
rescale(img, s, interp)
}
///|
/// Scale to cover `min_h x min_w`, preserving aspect ratio, cropping overflow.
///
/// - `min_h`, `min_w`: target dimensions to fully cover.
/// - `interp`: resampling method (`Nearest`, `Bilinear`, or `Bicubic`).
///
/// The image is scaled then center-cropped to `(min_h, min_w)`. Returns a
/// clone if the image is empty.
pub fn resize_to_cover(
img : Image,
min_h : Int,
min_w : Int,
interp : Interp,
) -> Image raise ImageError {
if img.is_empty() {
return img.clone()
}
let sy = min_h.to_double() / img.h.to_double()
let sx = min_w.to_double() / img.w.to_double()
let s = if sy > sx { sy } else { sx }
let scaled = rescale(img, s, interp)
let oy = clampi((scaled.h - min_h) / 2, 0, scaled.h)
let ox = clampi((scaled.w - min_w) / 2, 0, scaled.w)
crop(scaled, oy, ox, clampi(min_h, 0, scaled.h), clampi(min_w, 0, scaled.w))
}