///|
/// Sampling policy for thumbnails and preview images.
pub(all) enum SamplingMode {
Nearest
BoxAverage
} derive(Eq, Debug)
///|
/// Sample a pixel with integer coordinates, returning transparent black outside.
pub fn Canvas::sample(self : Canvas, x~ : Int, y~ : Int) -> Rgba {
match self.get(x~, y~) {
None => rgba(0, 0, 0, 0)
Some(color) => color
}
}
///|
/// Downsample or upscale using a selectable integer sampling policy.
pub fn Canvas::sampled(
self : Canvas,
width~ : Int,
height~ : Int,
mode? : SamplingMode = Nearest,
) -> 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.sample(x=x * self.width / width, y=y * self.height / height)
BoxAverage => average_sample(self, x, y, width, height)
}
}
}
result
}
}
///|
fn average_sample(
source : Canvas,
x : Int,
y : Int,
target_width : Int,
target_height : Int,
) -> Rgba {
let start_x = x * source.width / target_width
let end_x = (x + 1) * source.width / target_width - 1
let start_y = y * source.height / target_height
let end_y = (y + 1) * source.height / target_height - 1
let safe_end_x = if end_x < start_x { start_x } else { end_x }
let safe_end_y = if end_y < start_y { start_y } else { end_y }
let mut red = 0
let mut green = 0
let mut blue = 0
let mut alpha = 0
let mut count = 0
for row in start_y..<(safe_end_y + 1) {
for column in start_x..<(safe_end_x + 1) {
let color = source.sample(x=column, y=row)
red += color.red().to_int()
green += color.green().to_int()
blue += color.blue().to_int()
alpha += color.alpha().to_int()
count += 1
}
}
if count == 0 {
rgba(0, 0, 0, 0)
} else {
rgba(
(red / count).to_byte(),
(green / count).to_byte(),
(blue / count).to_byte(),
(alpha / count).to_byte(),
)
}
}
///|
/// Scale the long edge to `max_edge`, preserving aspect ratio.
pub fn Canvas::thumbnail(
self : Canvas,
max_edge : Int,
mode? : SamplingMode = Nearest,
) -> Canvas {
if max_edge <= 0 || self.width == 0 || self.height == 0 {
Canvas::new(width=0, height=0, fill=rgba(0, 0, 0, 0))
} else if self.width >= self.height {
let height = self.height * max_edge / self.width
self.sampled(
width=max_edge,
height=if height < 1 { 1 } else { height },
mode~,
)
} else {
let width = self.width * max_edge / self.height
self.sampled(
width=if width < 1 { 1 } else { width },
height=max_edge,
mode~,
)
}
}
///|
/// Compute the arithmetic mean color of all pixels.
pub fn Canvas::average_color(self : Canvas) -> Rgba {
if self.pixels.length() == 0 {
rgba(0, 0, 0, 0)
} else {
let mut red = 0
let mut green = 0
let mut blue = 0
let mut alpha = 0
for color in self.pixels {
red += color.red().to_int()
green += color.green().to_int()
blue += color.blue().to_int()
alpha += color.alpha().to_int()
}
let count = self.pixels.length()
rgba(
(red / count).to_byte(),
(green / count).to_byte(),
(blue / count).to_byte(),
(alpha / count).to_byte(),
)
}
}
///|
/// Return the most common exact color. This intentionally keeps the fast path
/// small; palette quantization remains the right choice for large images.
pub fn Canvas::dominant_color(self : Canvas) -> Rgba {
if self.pixels.length() == 0 {
rgba(0, 0, 0, 0)
} else {
let mut best = self.pixels[0]
let mut best_count = 0
for candidate in self.pixels {
let mut count = 0
for color in self.pixels {
if color == candidate {
count += 1
}
}
if count > best_count {
best = candidate
best_count = count
}
}
best
}
}
///|
/// Build an alpha-only mask where visible pixels become white.
pub fn Canvas::alpha_mask(self : Canvas, threshold? : Byte = 1) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for i in 0..= threshold {
rgba(255, 255, 255, alpha)
} else {
rgba(0, 0, 0, 0)
}
}
result
}
///|
/// Make an opaque silhouette using the source alpha channel.
pub fn Canvas::silhouette(
self : Canvas,
color? : Rgba = rgb(0, 0, 0),
threshold? : Byte = 1,
) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for i in 0..= threshold {
rgba(color.red(), color.green(), color.blue(), source.alpha())
} else {
rgba(0, 0, 0, 0)
}
}
result
}
///|
/// Return a compact hexadecimal-like RGB triplet for diagnostics.
pub fn Rgba::packed_rgb(self : Rgba) -> Int {
(self.red().to_int() << 16) |
(self.green().to_int() << 8) |
self.blue().to_int()
}
///|
/// Convert a packed RGB value into an opaque color.
pub fn rgb_from_packed(value : Int) -> Rgba {
rgba(
((value >> 16) & 255).to_byte(),
((value >> 8) & 255).to_byte(),
(value & 255).to_byte(),
255,
)
}