///|
pub(all) enum SquarePolicy {
Strict
CenterCrop
} derive(Eq, Debug)
///|
/// Return a square source image. `Strict` rejects a non-square input; the
/// opt-in crop policy retains the centered square region.
pub fn square_image(
image : Image,
policy? : SquarePolicy = Strict,
) -> Image raise ImageError {
if image.is_square() {
return image
}
if policy is Strict {
raise ImageError(NotSquare, "square image required")
}
let side = if image.width < image.height { image.width } else { image.height }
let left = (image.width - side) / 2
let top = (image.height - side) / 2
let pixels = Array::make(side * side * 4, b'\x00')
for y in 0.. Double {
let ax = if x < 0.0 { -x } else { x }
if ax >= 3.0 {
return 0.0
}
if ax < 0.0000001 {
return 1.0
}
let p = @math.PI
3.0 * @math.sin(p * ax) * @math.sin(p * ax / 3.0) / (p * p * ax * ax)
}
///|
fn clamp_byte(value : Double) -> Byte {
let rounded = @math.floor(value + 0.5).to_int()
(if rounded < 0 { 0 } else if rounded > 255 { 255 } else { rounded }).to_byte()
}
///|
/// Resize RGBA8 pixels with a Lanczos3 reconstruction filter in premultiplied
/// alpha space. Premultiplication prevents transparent colors from producing
/// dark fringes after resampling.
pub fn resize_lanczos3(
image : Image,
width : Int,
height : Int,
) -> Image raise ImageError {
if width <= 0 ||
height <= 0 ||
width > 0x3fffffff / height ||
width * height > 64 * 1024 * 1024 {
raise ImageError(InvalidDimensions, "resize dimensions are invalid")
}
if image.width == width && image.height == height {
return image
}
let result = Array::make(width * height * 4, b'\x00')
let sx = image.width.to_double() / width.to_double()
let sy = image.height.to_double() / height.to_double()
let scale_x = if sx > 1.0 { sx } else { 1.0 }
let scale_y = if sy > 1.0 { sy } else { 1.0 }
let radius_x = (3.0 * scale_x).to_int() + 1
let radius_y = (3.0 * scale_y).to_int() + 1
for y in 0..= 0 && iy < image.height {
let wy = lanczos_kernel((center_y - iy.to_double()) / scale_y)
for ix in first_x..= 0 && ix < image.width {
let weight = wy *
lanczos_kernel((center_x - ix.to_double()) / scale_x)
let source = (iy * image.width + ix) * 4
let alpha = image.pixels[source + 3].to_int().to_double() / 255.0
r = r + image.pixels[source].to_int().to_double() * alpha * weight
g = g +
image.pixels[source + 1].to_int().to_double() * alpha * weight
b = b +
image.pixels[source + 2].to_int().to_double() * alpha * weight
a = a + alpha * weight
weight_sum = weight_sum + weight
}
}
}
}
let output = (y * width + x) * 4
let normalized_a = if weight_sum == 0.0 { 0.0 } else { a / weight_sum }
result[output + 3] = clamp_byte(normalized_a * 255.0)
if a == 0.0 {
result[output] = b'\x00'
result[output + 1] = b'\x00'
result[output + 2] = b'\x00'
} else {
result[output] = clamp_byte(r / a)
result[output + 1] = clamp_byte(g / a)
result[output + 2] = clamp_byte(b / a)
}
}
}
Image::new(width, height, Bytes::from_array(result))
}
///|
pub(all) struct PngVariant {
size : Int
png : Bytes
}
///|
/// Encode caller-selected square PNG icon sizes in the requested order.
pub fn encode_png_sizes(
image : Image,
sizes : Array[Int],
compression_level? : Int = 6,
) -> Array[PngVariant] raise ImageError {
checked_square(image)
let output = []
for size in sizes {
if size <= 0 {
raise ImageError(InvalidDimensions, "PNG output size must be positive")
}
let resized = resize_lanczos3(image, size, size)
output.push({ size, png: encode_png(resized, compression_level~) })
}
output
}