use super::alpha::grayscale_on_white;
use super::monochrome::burns_at_threshold;
use crate::errors::{Error, Result};
use crate::geometry::{LabelPx, Rect, SafeArea};
use crate::types::Rotation;
use image::{DynamicImage, GenericImageView, GrayImage, RgbaImage, imageops};
pub fn rotate(img: DynamicImage, rotation: Rotation) -> DynamicImage {
match rotation {
Rotation::Deg0 => img,
Rotation::Deg90 => img.rotate90(),
Rotation::Deg180 => img.rotate180(),
Rotation::Deg270 => img.rotate270(),
}
}
pub fn trim_white(img: DynamicImage, threshold: u8) -> DynamicImage {
let ink = image_luma_bounds(&img, |luma| luma <= threshold);
crop_to_bounds(img, ink)
}
pub(crate) fn trim_for_print(img: DynamicImage, threshold: u8, dither: bool) -> DynamicImage {
let ink = if dither {
image_luma_bounds(&img, |luma| luma < u8::MAX).map(|bounds| Rect {
x: 0,
y: bounds.y,
w: img.width(),
h: bounds.h,
})
} else {
image_luma_bounds(&img, |luma| burns_at_threshold(luma, threshold))
};
crop_to_bounds(img, ink)
}
pub(super) fn crop_to_bounds(img: DynamicImage, bounds: Option<Rect>) -> DynamicImage {
let (width, height) = img.dimensions();
let Some(bounds) = bounds else {
return img;
};
if bounds.x == 0 && bounds.y == 0 && bounds.w == width && bounds.h == height {
return img;
}
img.crop_imm(bounds.x, bounds.y, bounds.w, bounds.h)
}
fn image_luma_bounds(img: &DynamicImage, predicate: impl FnMut(u8) -> bool) -> Option<Rect> {
if let Some(gray) = img.as_luma8() {
return luma_bounds_by(
gray.enumerate_pixels()
.map(|(x, y, pixel)| (x, y, pixel[0])),
predicate,
);
}
if let Some(rgb) = img.as_rgb8() {
return striped_rgb8_luma_bounds(rgb, predicate);
}
if !img.has_alpha() {
let gray = img.to_luma8();
return luma_bounds_by(
gray.enumerate_pixels()
.map(|(x, y, pixel)| (x, y, pixel[0])),
predicate,
);
}
striped_luma_bounds(img, predicate, grayscale_on_white)
}
fn striped_rgb8_luma_bounds(
rgb: &image::RgbImage,
mut predicate: impl FnMut(u8) -> bool,
) -> Option<Rect> {
const STRIPE_ROWS: u32 = 64;
let (width, height) = rgb.dimensions();
let row_bytes = width as usize * 3;
let color_space = rgb.color_space();
let mut bounds = LumaBounds::new();
let mut y = 0;
while width != 0 && y < height {
let rows = (height - y).min(STRIPE_ROWS);
let start = y as usize * row_bytes;
let end = (y + rows) as usize * row_bytes;
let mut stripe = image::RgbImage::from_raw(width, rows, rgb.as_raw()[start..end].to_vec())
.expect("a source-aligned RGB stripe has valid dimensions");
stripe
.set_color_space(color_space)
.expect("the source RGB image already has a valid color space");
let gray = DynamicImage::ImageRgb8(stripe).into_luma8();
for (x, stripe_y, pixel) in gray.enumerate_pixels() {
if predicate(pixel[0]) {
bounds.include(x, y + stripe_y);
}
}
y += rows;
}
bounds.finish()
}
fn striped_luma_bounds(
img: &DynamicImage,
mut predicate: impl FnMut(u8) -> bool,
mut into_gray: impl FnMut(DynamicImage) -> GrayImage,
) -> Option<Rect> {
const STRIPE_ROWS: u32 = 64;
let (width, height) = img.dimensions();
let mut bounds = LumaBounds::new();
let mut y = 0;
while width != 0 && y < height {
let rows = (height - y).min(STRIPE_ROWS);
let gray = into_gray(img.crop_imm(0, y, width, rows));
for (x, stripe_y, pixel) in gray.enumerate_pixels() {
if predicate(pixel[0]) {
bounds.include(x, y + stripe_y);
}
}
y += rows;
}
bounds.finish()
}
pub(super) fn luma_bounds_by(
pixels: impl Iterator<Item = (u32, u32, u8)>,
mut predicate: impl FnMut(u8) -> bool,
) -> Option<Rect> {
let mut bounds = LumaBounds::new();
for (x, y, luma) in pixels {
if predicate(luma) {
bounds.include(x, y);
}
}
bounds.finish()
}
struct LumaBounds {
x0: u32,
y0: u32,
x1: u32,
y1: u32,
}
impl LumaBounds {
fn new() -> Self {
Self {
x0: u32::MAX,
y0: u32::MAX,
x1: 0,
y1: 0,
}
}
fn include(&mut self, x: u32, y: u32) {
self.x0 = self.x0.min(x);
self.y0 = self.y0.min(y);
self.x1 = self.x1.max(x);
self.y1 = self.y1.max(y);
}
fn finish(self) -> Option<Rect> {
(self.x0 != u32::MAX).then(|| Rect {
x: self.x0,
y: self.y0,
w: self.x1 - self.x0 + 1,
h: self.y1 - self.y0 + 1,
})
}
}
pub fn ink_bounds(gray: &GrayImage, threshold: u8) -> Option<Rect> {
luma_bounds_by(
gray.enumerate_pixels()
.map(|(x, y, pixel)| (x, y, pixel[0])),
|luma| luma <= threshold,
)
}
pub fn fit_width(img: DynamicImage, max_width: u32) -> DynamicImage {
let (w, h) = img.dimensions();
if w <= max_width {
return img;
}
let new_h = ((h as f64) * (max_width as f64) / (w as f64)).round() as u32;
match img {
DynamicImage::ImageLuma8(gray) => DynamicImage::ImageLuma8(imageops::resize(
&gray,
max_width,
new_h.max(1),
imageops::FilterType::Triangle,
)),
image => DynamicImage::ImageRgba8(imageops::resize(
&image,
max_width,
new_h.max(1),
imageops::FilterType::Triangle,
)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ContentBox {
canvas_w: u32,
canvas_h: u32,
origin_x: u32,
origin_y: u32,
margin: u32,
width: u32,
height: u32,
}
impl ContentBox {
fn for_label(label: LabelPx, safe: SafeArea, margin: u32) -> Result<Self> {
let area = safe.content(label).ok_or_else(|| {
Error::invalid_label(format!(
"safe area (top {}, bottom {}, left {}, right {}) leaves no content on a {}x{}px label",
safe.top,
safe.bottom,
safe.left,
safe.right,
label.width_px,
label.height_px
))
})?;
Ok(Self::in_rect(label, area, margin))
}
fn in_rect(label: LabelPx, area: Rect, margin: u32) -> Self {
let canvas_w = label.width_px.max(1);
let canvas_h = label.height_px.max(1);
let aw = area.w.max(1);
let ah = area.h.max(1);
let margin = margin.min(aw / 4).min(ah / 4);
Self {
canvas_w,
canvas_h,
origin_x: area.x + margin,
origin_y: area.y + margin,
margin,
width: aw.saturating_sub(margin * 2).max(1),
height: ah.saturating_sub(margin * 2).max(1),
}
}
fn white_canvas(&self) -> RgbaImage {
RgbaImage::from_pixel(
self.canvas_w,
self.canvas_h,
image::Rgba([255, 255, 255, 255]),
)
}
}
fn scaled_dimensions(img: &DynamicImage, scale: f64) -> (u32, u32) {
let (iw, ih) = img.dimensions();
(
((iw as f64) * scale).round().max(1.0) as u32,
((ih as f64) * scale).round().max(1.0) as u32,
)
}
fn cover_crop(iw: u32, ih: u32, target_w: u32, target_h: u32) -> Rect {
let center_aligned = |source: u32, crop: u32| {
if crop < source && (source - crop) % 2 == 1 {
crop + 1
} else {
crop
}
};
let source_cross = u64::from(iw) * u64::from(target_h);
let target_cross = u64::from(target_w) * u64::from(ih);
if source_cross > target_cross {
let numerator = u64::from(ih) * u64::from(target_w);
let crop_w = center_aligned(
iw,
numerator
.div_ceil(u64::from(target_h))
.clamp(1, u64::from(iw)) as u32,
);
Rect {
x: (iw - crop_w) / 2,
y: 0,
w: crop_w,
h: ih,
}
} else if source_cross < target_cross {
let numerator = u64::from(iw) * u64::from(target_h);
let crop_h = center_aligned(
ih,
numerator
.div_ceil(u64::from(target_w))
.clamp(1, u64::from(ih)) as u32,
);
Rect {
x: 0,
y: (ih - crop_h) / 2,
w: iw,
h: crop_h,
}
} else {
Rect {
x: 0,
y: 0,
w: iw,
h: ih,
}
}
}
pub fn fill_label(
img: DynamicImage,
label: LabelPx,
safe: SafeArea,
margin: u32,
) -> Result<DynamicImage> {
let bx = ContentBox::for_label(label, safe, margin)?;
let (iw, ih) = img.dimensions();
let crop = cover_crop(iw, ih, bx.width, bx.height);
let source = imageops::crop_imm(&img, crop.x, crop.y, crop.w, crop.h);
let scale = f64::max(
bx.width as f64 / crop.w as f64,
bx.height as f64 / crop.h as f64,
);
let nw = ((crop.w as f64) * scale).round().max(1.0) as u32;
let nh = ((crop.h as f64) * scale).round().max(1.0) as u32;
let resized = imageops::resize(
&*source,
nw.max(bx.width),
nh.max(bx.height),
imageops::FilterType::CatmullRom,
);
let visible = imageops::crop_imm(
&resized,
resized.width().saturating_sub(bx.width) / 2,
resized.height().saturating_sub(bx.height) / 2,
bx.width,
bx.height,
);
let mut canvas = bx.white_canvas();
imageops::overlay(
&mut canvas,
&*visible,
bx.origin_x as i64,
bx.origin_y as i64,
);
Ok(DynamicImage::ImageRgba8(canvas))
}
pub fn contain_label(
img: DynamicImage,
label: LabelPx,
safe: SafeArea,
margin: u32,
) -> Result<DynamicImage> {
let bx = ContentBox::for_label(label, safe, margin)?;
let (iw, ih) = img.dimensions();
let scale = f64::min(bx.width as f64 / iw as f64, bx.height as f64 / ih as f64);
let (nw, nh) = scaled_dimensions(&img, scale);
let resized = imageops::resize(&img, nw, nh, imageops::FilterType::CatmullRom);
let mut canvas = bx.white_canvas();
imageops::overlay(
&mut canvas,
&resized,
(bx.origin_x + bx.width.saturating_sub(nw) / 2) as i64,
(bx.origin_y + bx.height.saturating_sub(nh) / 2) as i64,
);
Ok(DynamicImage::ImageRgba8(canvas))
}