///|
/// Resampling filters selected explicitly for quality or speed.
pub(all) enum ResampleFilter {
NearestPixel
BoxAverage
BilinearInteger
} derive(Eq, Debug)
///|
pub(all) struct ResampleReport {
input_width : Int
input_height : Int
output_width : Int
output_height : Int
filter : ResampleFilter
input_pixels : Int
output_pixels : Int
} derive(Debug)
///|
pub fn ResampleReport::input_size(self : ResampleReport) -> (Int, Int) {
(self.input_width, self.input_height)
}
///|
pub fn ResampleReport::output_size(self : ResampleReport) -> (Int, Int) {
(self.output_width, self.output_height)
}
///|
pub fn ResampleReport::filter(self : ResampleReport) -> ResampleFilter {
self.filter
}
///|
pub fn ResampleReport::input_pixels(self : ResampleReport) -> Int {
self.input_pixels
}
///|
pub fn ResampleReport::output_pixels(self : ResampleReport) -> Int {
self.output_pixels
}
///|
fn weighted_channel(
left : Int,
right : Int,
numerator : Int,
denominator : Int,
) -> Byte {
if denominator <= 0 {
left.to_byte()
} else {
((left * (denominator - numerator) + right * numerator) / denominator).to_byte()
}
}
///|
fn bilinear_sample(
canvas : Canvas,
x_num : Int,
y_num : Int,
width : Int,
height : Int,
) -> Rgba {
let source_x = x_num * canvas.width / width
let source_y = y_num * canvas.height / height
let next_x = if source_x + 1 >= canvas.width {
source_x
} else {
source_x + 1
}
let next_y = if source_y + 1 >= canvas.height {
source_y
} else {
source_y + 1
}
let x_fraction = x_num * canvas.width % width
let y_fraction = y_num * canvas.height % height
let top_left = canvas.pixels[source_x + source_y * canvas.width]
let top_right = canvas.pixels[next_x + source_y * canvas.width]
let bottom_left = canvas.pixels[source_x + next_y * canvas.width]
let bottom_right = canvas.pixels[next_x + next_y * canvas.width]
let top = rgba(
weighted_channel(
top_left.red().to_int(),
top_right.red().to_int(),
x_fraction,
width,
),
weighted_channel(
top_left.green().to_int(),
top_right.green().to_int(),
x_fraction,
width,
),
weighted_channel(
top_left.blue().to_int(),
top_right.blue().to_int(),
x_fraction,
width,
),
weighted_channel(
top_left.alpha().to_int(),
top_right.alpha().to_int(),
x_fraction,
width,
),
)
let bottom = rgba(
weighted_channel(
bottom_left.red().to_int(),
bottom_right.red().to_int(),
x_fraction,
width,
),
weighted_channel(
bottom_left.green().to_int(),
bottom_right.green().to_int(),
x_fraction,
width,
),
weighted_channel(
bottom_left.blue().to_int(),
bottom_right.blue().to_int(),
x_fraction,
width,
),
weighted_channel(
bottom_left.alpha().to_int(),
bottom_right.alpha().to_int(),
x_fraction,
width,
),
)
rgba(
weighted_channel(
top.red().to_int(),
bottom.red().to_int(),
y_fraction,
height,
),
weighted_channel(
top.green().to_int(),
bottom.green().to_int(),
y_fraction,
height,
),
weighted_channel(
top.blue().to_int(),
bottom.blue().to_int(),
y_fraction,
height,
),
weighted_channel(
top.alpha().to_int(),
bottom.alpha().to_int(),
y_fraction,
height,
),
)
}
///|
fn box_sample(
canvas : Canvas,
left : Int,
top : Int,
right : Int,
bottom : Int,
) -> Rgba {
let mut red = 0
let mut green = 0
let mut blue = 0
let mut alpha = 0
let mut count = 0
for y in top.. Canvas {
if width < 0 || height < 0 {
panic()
}
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
if width == 0 || height == 0 || self.width == 0 || self.height == 0 {
result
} else {
for y in 0..
self.pixels[x * self.width / width +
y * self.height / height * self.width]
BilinearInteger => bilinear_sample(self, x, y, width, height)
BoxAverage => {
let left = x * self.width / width
let right = (x + 1) * self.width / width + 1
let top = y * self.height / height
let bottom = (y + 1) * self.height / height + 1
box_sample(
self,
left,
top,
if right > self.width {
self.width
} else {
right
},
if bottom > self.height {
self.height
} else {
bottom
},
)
}
}
result.pixels[x + y * width] = color
}
}
result
}
}
///|
/// Resize and return metrics useful for export logs.
pub fn Canvas::resample_with_report(
self : Canvas,
width~ : Int,
height~ : Int,
filter? : ResampleFilter = BilinearInteger,
) -> (Canvas, ResampleReport) {
let result = self.resample(width~, height~, filter~)
(
result,
ResampleReport::{
input_width: self.width,
input_height: self.height,
output_width: result.width,
output_height: result.height,
filter,
input_pixels: self.len(),
output_pixels: result.len(),
},
)
}
///|
/// Downsample by an integer factor using box averaging.
pub fn Canvas::downsample(self : Canvas, factor : Int) -> Canvas {
let safe = if factor < 1 { 1 } else { factor }
self.resample(
width=self.width / safe,
height=self.height / safe,
filter=BoxAverage,
)
}
///|
/// Upsample by an integer factor using nearest pixels for crisp sprites.
pub fn Canvas::upscale(self : Canvas, factor : Int) -> Canvas {
let safe = if factor < 1 { 1 } else { factor }
self.resample(
width=self.width * safe,
height=self.height * safe,
filter=NearestPixel,
)
}
///|
/// Fit an image to an exact output size using the chosen filter.
pub fn Canvas::fit_resample(
self : Canvas,
width : Int,
height : Int,
filter : ResampleFilter,
) -> Canvas {
self.resample(width~, height~, filter~)
}
///|
/// Resample every frame while keeping the animation timeline.
pub fn Animation::resample_frames_with_filter(
self : Animation,
width : Int,
height : Int,
filter? : ResampleFilter = BilinearInteger,
) -> Animation {
let result = Animation::new(width~, height~)
.with_loop_count(loop_count=self.loop_count)
.with_background(background=self.background)
for frame in self.frames {
ignore(
result.add_frame(
frame
.with_canvas(frame.canvas().resample(width~, height~, filter~))
.with_offset(left=0, top=0),
),
)
}
result
}
///|
/// Use pixel aspect compensation for low-resolution GIF previews.
pub fn Canvas::pixel_aspect(
self : Canvas,
horizontal_per_mille : Int,
vertical_per_mille : Int,
) -> Canvas {
let horizontal = if horizontal_per_mille < 1 {
1
} else {
horizontal_per_mille
}
let vertical = if vertical_per_mille < 1 { 1 } else { vertical_per_mille }
self.resample(
width=self.width * horizontal / 1000,
height=self.height * vertical / 1000,
filter=BilinearInteger,
)
}