use teksilo_tokens::Alignment;
use crate::geometry::{Point, Rect, Size, Transform2D};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ImageOrientation {
#[default]
Normal,
FlipHorizontal,
Rotate180,
FlipVertical,
Transpose,
Rotate90,
Transverse,
Rotate270,
}
impl ImageOrientation {
pub const fn from_exif(value: u16) -> Option<Self> {
Some(match value {
1 => Self::Normal,
2 => Self::FlipHorizontal,
3 => Self::Rotate180,
4 => Self::FlipVertical,
5 => Self::Transpose,
6 => Self::Rotate90,
7 => Self::Transverse,
8 => Self::Rotate270,
_ => return None,
})
}
pub const fn from_tiff(value: u16) -> Self {
match Self::from_exif(value) {
Some(orientation) => orientation,
None => Self::Normal,
}
}
pub const fn swaps_axes(self) -> bool {
matches!(
self,
Self::Transpose | Self::Rotate90 | Self::Transverse | Self::Rotate270
)
}
pub const fn is_identity(self) -> bool {
matches!(self, Self::Normal)
}
pub const fn displayed_size(self, width: u32, height: u32) -> (u32, u32) {
if self.swaps_axes() {
(height, width)
} else {
(width, height)
}
}
const fn source_pixel(self, xd: u32, yd: u32, width: u32, height: u32) -> (u32, u32) {
match self {
Self::Normal => (xd, yd),
Self::FlipHorizontal => (width - 1 - xd, yd),
Self::Rotate180 => (width - 1 - xd, height - 1 - yd),
Self::FlipVertical => (xd, height - 1 - yd),
Self::Transpose => (yd, xd),
Self::Rotate90 => (yd, height - 1 - xd),
Self::Transverse => (width - 1 - yd, height - 1 - xd),
Self::Rotate270 => (width - 1 - yd, xd),
}
}
fn source_point(self, xd: f32, yd: f32, width: f32, height: f32) -> (f32, f32) {
match self {
Self::Normal => (xd, yd),
Self::FlipHorizontal => (width - xd, yd),
Self::Rotate180 => (width - xd, height - yd),
Self::FlipVertical => (xd, height - yd),
Self::Transpose => (yd, xd),
Self::Rotate90 => (yd, height - xd),
Self::Transverse => (width - yd, height - xd),
Self::Rotate270 => (width - yd, xd),
}
}
const fn displayed_pixel(self, x: u32, y: u32, width: u32, height: u32) -> (u32, u32) {
match self {
Self::Normal => (x, y),
Self::FlipHorizontal => (width - 1 - x, y),
Self::Rotate180 => (width - 1 - x, height - 1 - y),
Self::FlipVertical => (x, height - 1 - y),
Self::Transpose => (y, x),
Self::Rotate90 => (height - 1 - y, x),
Self::Transverse => (height - 1 - y, width - 1 - x),
Self::Rotate270 => (y, width - 1 - x),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ImageFit {
#[default]
Contain,
Cover,
Fill,
ScaleDown,
None,
}
impl ImageFit {
pub fn fitted_rect(self, content: Size, bounds: Rect, alignment: Alignment, rtl: bool) -> Rect {
let (img_w, img_h) = (content.width, content.height);
if img_w <= 0.0 || img_h <= 0.0 {
return bounds;
}
let (w, h) = match self {
Self::Fill => (bounds.width, bounds.height),
Self::Contain => {
let scale = (bounds.width / img_w).min(bounds.height / img_h);
(img_w * scale, img_h * scale)
}
Self::Cover => {
let scale = (bounds.width / img_w).max(bounds.height / img_h);
(img_w * scale, img_h * scale)
}
Self::ScaleDown => {
let scale = (bounds.width / img_w).min(bounds.height / img_h).min(1.0);
(img_w * scale, img_h * scale)
}
Self::None => (img_w, img_h),
};
let x = bounds.x + alignment.horizontal.resolve(w, bounds.width, rtl);
let y = bounds.y + alignment.vertical.resolve(h, bounds.height);
Rect::new(x, y, w, h)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct PixelRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl PixelRect {
pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub const fn full(width: u32, height: u32) -> Self {
Self::new(0, 0, width, height)
}
pub const fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0
}
pub const fn area(&self) -> u64 {
self.width as u64 * self.height as u64
}
pub const fn fits_in(&self, width: u32, height: u32) -> bool {
self.x as u64 + self.width as u64 <= width as u64
&& self.y as u64 + self.height as u64 <= height as u64
}
pub fn union(&self, other: &PixelRect) -> PixelRect {
if self.is_empty() {
return *other;
}
if other.is_empty() {
return *self;
}
let x0 = self.x.min(other.x);
let y0 = self.y.min(other.y);
let x1 = (self.x as u64 + self.width as u64).max(other.x as u64 + other.width as u64);
let y1 = (self.y as u64 + self.height as u64).max(other.y as u64 + other.height as u64);
PixelRect::new(x0, y0, saturate(x1 - x0 as u64), saturate(y1 - y0 as u64))
}
pub fn intersect(&self, other: &PixelRect) -> Option<PixelRect> {
let x0 = self.x.max(other.x);
let y0 = self.y.max(other.y);
let x1 = (self.x as u64 + self.width as u64).min(other.x as u64 + other.width as u64);
let y1 = (self.y as u64 + self.height as u64).min(other.y as u64 + other.height as u64);
(x1 > x0 as u64 && y1 > y0 as u64)
.then(|| PixelRect::new(x0, y0, saturate(x1 - x0 as u64), saturate(y1 - y0 as u64)))
}
}
fn saturate(v: u64) -> u32 {
u32::try_from(v).unwrap_or(u32::MAX)
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ImageGeometry {
pub source: (u32, u32),
pub orientation: ImageOrientation,
pub content: Rect,
pub bounds: Rect,
}
impl ImageGeometry {
pub fn new(
source: (u32, u32),
orientation: ImageOrientation,
content: Rect,
bounds: Rect,
) -> Self {
Self {
source,
orientation,
content,
bounds,
}
}
pub fn compute(
bounds: Size,
source: (u32, u32),
fit: ImageFit,
alignment: Alignment,
rtl: bool,
orientation: ImageOrientation,
) -> Self {
let (w, h) = orientation.displayed_size(source.0, source.1);
let bounds = Rect::new(0.0, 0.0, bounds.width, bounds.height);
let content = fit.fitted_rect(Size::new(w as f32, h as f32), bounds, alignment, rtl);
Self::new(source, orientation, content, bounds)
}
pub fn snapped(self, to_device: Transform2D) -> Self {
if !Self::snaps_under(to_device) {
return self;
}
let [a, _, _, d, tx, ty] = to_device.m;
let snap =
|v: f32, scale: f32, offset: f32| ((v * scale + offset).round() - offset) / scale;
let axis = |origin: f32, extent: f32, scale: f32, offset: f32| {
let (s0, s1) = (
snap(origin, scale, offset),
snap(origin + extent, scale, offset),
);
let (s0, s1) = (s0.min(s1), s0.max(s1));
if s1 - s0 > 0.0 {
(s0, s1 - s0)
} else {
(origin, extent)
}
};
let c = self.content;
let (x, width) = axis(c.x, c.width, a, tx);
let (y, height) = axis(c.y, c.height, d, ty);
Self {
content: Rect::new(x, y, width, height),
..self
}
}
pub fn snaps_under(to_device: Transform2D) -> bool {
let [a, b, c, d, _, _] = to_device.m;
b == 0.0 && c == 0.0 && a != 0.0 && d != 0.0 && a.is_finite() && d.is_finite()
}
pub fn displayed(&self) -> (u32, u32) {
self.orientation
.displayed_size(self.source.0, self.source.1)
}
pub fn visible(&self) -> Option<Rect> {
intersect(self.content, self.bounds)
}
pub fn map_to_source(&self, local: Point) -> Option<(u32, u32)> {
let b = self.bounds;
if !(local.x >= b.x
&& local.x < b.x + b.width
&& local.y >= b.y
&& local.y < b.y + b.height)
{
return None;
}
let (u, v) = self.unit(local)?;
if !((0.0..1.0).contains(&u) && (0.0..1.0).contains(&v)) {
return None;
}
self.pixel_at(u, v)
}
pub fn map_to_source_clamped(&self, local: Point) -> Option<(u32, u32)> {
let (u, v) = self.unit(local)?;
self.pixel_at(u.clamp(0.0, 1.0), v.clamp(0.0, 1.0))
}
pub fn map_to_source_f32(&self, local: Point) -> Option<(f32, f32)> {
let (u, v) = self.unit(local)?;
let (wd, hd) = self.displayed();
let (w, h) = self.source;
Some(
self.orientation
.source_point(u * wd as f32, v * hd as f32, w as f32, h as f32),
)
}
pub fn map_from_source(&self, rect: PixelRect) -> Option<Rect> {
let (w, h) = self.source;
if rect.is_empty() || !rect.fits_in(w, h) {
return None;
}
let (wd, hd) = self.displayed();
if self.content.width <= 0.0 || self.content.height <= 0.0 {
return None;
}
let (ax, ay) = self.orientation.displayed_pixel(rect.x, rect.y, w, h);
let (bx, by) = self.orientation.displayed_pixel(
rect.x + rect.width - 1,
rect.y + rect.height - 1,
w,
h,
);
let (x0, x1) = (ax.min(bx), ax.max(bx) + 1);
let (y0, y1) = (ay.min(by), ay.max(by) + 1);
let c = self.content;
let sx = c.width / wd as f32;
let sy = c.height / hd as f32;
Some(Rect::new(
c.x + x0 as f32 * sx,
c.y + y0 as f32 * sy,
(x1 - x0) as f32 * sx,
(y1 - y0) as f32 * sy,
))
}
fn unit(&self, local: Point) -> Option<(f32, f32)> {
let c = self.content;
let (w, h) = self.source;
if w == 0 || h == 0 || c.width <= 0.0 || c.height <= 0.0 {
return None;
}
Some(((local.x - c.x) / c.width, (local.y - c.y) / c.height))
}
fn pixel_at(&self, u: f32, v: f32) -> Option<(u32, u32)> {
let (w, h) = self.source;
let (wd, hd) = self.displayed();
let xd = ((u * wd as f32).floor() as u32).min(wd - 1);
let yd = ((v * hd as f32).floor() as u32).min(hd - 1);
Some(self.orientation.source_pixel(xd, yd, w, h))
}
}
pub fn oriented_crop(
content: Rect,
clip: Rect,
orientation: ImageOrientation,
) -> Option<(Rect, [[f32; 2]; 4])> {
if content.width <= 0.0 || content.height <= 0.0 {
return None;
}
let visible = intersect(content, clip)?;
let u0 = (visible.x - content.x) / content.width;
let v0 = (visible.y - content.y) / content.height;
let u1 = (visible.x + visible.width - content.x) / content.width;
let v1 = (visible.y + visible.height - content.y) / content.height;
let corner = |u: f32, v: f32| {
let (s, t) = orientation.source_point(u, v, 1.0, 1.0);
[s.clamp(0.0, 1.0), t.clamp(0.0, 1.0)]
};
Some((
visible,
[
corner(u0, v0),
corner(u1, v0),
corner(u1, v1),
corner(u0, v1),
],
))
}
fn intersect(a: Rect, b: Rect) -> Option<Rect> {
let x0 = a.x.max(b.x);
let y0 = a.y.max(b.y);
let x1 = (a.x + a.width).min(b.x + b.width);
let y1 = (a.y + a.height).min(b.y + b.height);
(x1 > x0 && y1 > y0).then(|| Rect::new(x0, y0, x1 - x0, y1 - y0))
}
#[cfg(test)]
mod tests;