use std::path;
use printpdf::image_crate::GenericImageView;
use crate::error::{Context as _, Error, ErrorKind};
use crate::{render, style};
use crate::{Alignment, Context, Element, Mm, Position, RenderResult, Rotation, Scale, Size};
#[derive(Clone)]
pub struct Image {
data: printpdf::image_crate::DynamicImage,
alignment: Alignment,
position: Option<Position>,
scale: Scale,
rotation: Rotation,
dpi: Option<f64>,
}
impl Image {
pub fn from_dynamic_image(data: printpdf::image_crate::DynamicImage) -> Result<Self, Error> {
if data.color().has_alpha() {
Err(Error::new(
"Images with an alpha channel are not supported",
ErrorKind::InvalidData,
))
} else {
Ok(Image {
data,
alignment: Alignment::default(),
position: None,
scale: Scale::default(),
rotation: Rotation::default(),
dpi: None,
})
}
}
fn from_image_reader<R>(reader: printpdf::image_crate::io::Reader<R>) -> Result<Self, Error>
where
R: std::io::BufRead,
R: std::io::Read,
R: std::io::Seek,
{
let image = reader
.with_guessed_format()
.context("Could not determine image format")?
.decode()
.context("Could not decode image")?;
Self::from_dynamic_image(image)
}
pub fn from_reader<R>(reader: R) -> Result<Self, Error>
where
R: std::io::BufRead,
R: std::io::Read,
R: std::io::Seek,
{
Self::from_image_reader(printpdf::image_crate::io::Reader::new(reader))
}
pub fn from_path(path: impl AsRef<path::Path>) -> Result<Self, Error> {
let path = path.as_ref();
let reader = printpdf::image_crate::io::Reader::open(path)
.with_context(|| format!("Could not read image from path {}", path.display()))?;
Self::from_image_reader(reader)
}
pub fn set_position(&mut self, position: impl Into<Position>) {
self.position = Some(position.into());
}
pub fn with_position(mut self, position: impl Into<Position>) -> Self {
self.set_position(position);
self
}
pub fn set_scale(&mut self, scale: impl Into<Scale>) {
self.scale = scale.into();
}
pub fn with_scale(mut self, scale: impl Into<Scale>) -> Self {
self.set_scale(scale);
self
}
pub fn set_alignment(&mut self, alignment: impl Into<Alignment>) {
self.alignment = alignment.into();
}
pub fn with_alignment(mut self, alignment: impl Into<Alignment>) -> Self {
self.set_alignment(alignment);
self
}
fn get_offset(&self, width: Mm, max_width: Mm) -> Position {
let horizontal_offset = match self.alignment {
Alignment::Left => Mm::default(),
Alignment::Center => (max_width - width) / 2.0,
Alignment::Right => max_width - width,
};
Position::new(horizontal_offset, 0)
}
fn get_size(&self) -> Size {
let mmpi: f64 = 25.4; let dpi: f64 = self.dpi.unwrap_or(300.0);
let (px_width, px_height) = self.data.dimensions();
let (scale_width, scale_height): (f64, f64) = (self.scale.x, self.scale.y);
Size::new(
mmpi * ((scale_width * px_width as f64) / dpi),
mmpi * ((scale_height * px_height as f64) / dpi),
)
}
pub fn set_clockwise_rotation(&mut self, rotation: impl Into<Rotation>) {
self.rotation = rotation.into();
}
pub fn with_clockwise_rotation(mut self, rotation: impl Into<Rotation>) -> Self {
self.set_clockwise_rotation(rotation);
self
}
pub fn set_dpi(&mut self, dpi: f64) {
self.dpi = Some(dpi);
}
pub fn with_dpi(mut self, dpi: f64) -> Self {
self.set_dpi(dpi);
self
}
}
impl Element for Image {
fn render(
&mut self,
_context: &Context,
area: render::Area<'_>,
_style: style::Style,
) -> Result<RenderResult, Error> {
let mut result = RenderResult::default();
let true_size = self.get_size();
let (bb_origin, bb_size) = bounding_box_offset_and_size(&self.rotation, &true_size);
let mut position: Position = if let Some(position) = self.position {
position
} else {
result.size = bb_size;
self.get_offset(bb_size.width, area.size().width)
};
position += bb_origin;
area.add_image(&self.data, position, self.scale, self.rotation, self.dpi);
result.has_more = false;
Ok(result)
}
}
#[allow(clippy::manual_range_contains)]
fn bounding_box_offset_and_size(rotation: &Rotation, size: &Size) -> (Position, Size) {
let alpha = rotation.degrees.to_radians();
let beta = (90.0 - rotation.degrees).to_radians();
let sa = alpha.sin();
let sb = beta.sin();
let width = (size.width.0 * sb).abs() + (size.height.0 * sa).abs();
let height = (size.height.0 * sb).abs() + (size.width.0 * sa).abs();
let bb_size = Size::new(width, height);
let bb_position = if rotation.degrees < -180.0 {
unreachable!(
"Rotations must be in the range -180.0..=180.0, but got: {}",
rotation.degrees
);
} else if rotation.degrees <= -90.0 {
Position::new(size.width.0 * alpha.cos().abs(), 0)
} else if rotation.degrees <= 0.0 {
Position::new(0, size.height.0 * alpha.cos())
} else if rotation.degrees <= 90.0 {
Position::new(size.height.0 * beta.cos(), bb_size.height.0)
} else if rotation.degrees <= 180.0 {
Position::new(bb_size.width.0, size.width.0 * beta.cos())
} else {
unreachable!(
"Rotations must be in the range -180.0..=180.0, but got: {}",
rotation.degrees
);
};
(bb_position, bb_size)
}
#[cfg(test)]
mod tests {
use super::bounding_box_offset_and_size;
use crate::{Position, Rotation, Size};
use float_cmp::approx_eq;
macro_rules! assert_approx_eq {
($typ:ty, $lhs:expr, $rhs:expr) => {
let left = $lhs;
let right = $rhs;
assert!(
approx_eq!($typ, left, right, epsilon = 100.0 * f64::EPSILON, ulps = 10),
"assertion failed: `(left approx_eq right)`
left: `{:?}`,
right: `{:?}`",
left,
right
);
};
}
fn test_position(size: Size, rotation: f64, position: Position) {
println!("rotation = {}", rotation);
let rotation = Rotation::from(rotation);
assert_approx_eq!(
Position,
position,
bounding_box_offset_and_size(&rotation, &size).0
);
}
#[test]
fn test_bounding_box_size_square_0_deg() {
let size = Size::new(100, 100);
for rotation in &[-180.0, -90.0, 0.0, 90.0, 180.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(Size, size, bounding_box_offset_and_size(&rotation, &size).1);
}
}
#[test]
fn test_bounding_box_size_square_30_deg() {
let size = Size::new(100, 100);
let bb_width = (60.0f64.to_radians().sin() + 30.0f64.to_radians().sin()) * size.width.0;
let bb_size = Size::new(bb_width, bb_width);
for rotation in &[-150.0, -120.0, -30.0, -60.0, 30.0, 60.0, 120.0, 150.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(
Size,
bb_size,
bounding_box_offset_and_size(&rotation, &size).1
);
}
}
#[test]
fn test_bounding_box_size_square_45_deg() {
let size = Size::new(100, 100);
let bb_width = (2.0f64 * size.width.0.powf(2.0)).sqrt();
let bb_size = Size::new(bb_width, bb_width);
for rotation in &[-135.0, -45.0, 45.0, 135.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(
Size,
bb_size,
bounding_box_offset_and_size(&rotation, &size).1
);
}
}
#[test]
fn test_bounding_box_position_square_30_deg() {
let size = Size::new(100, 100);
let bb_width =
30.0f64.to_radians().sin() * size.width.0 + 60.0f64.to_radians().sin() * size.height.0;
let w30 = 30.0f64.to_radians().cos() * size.width.0;
let w60 = 60.0f64.to_radians().cos() * size.width.0;
test_position(size, -150.0, Position::new(w30, 0));
test_position(size, -120.0, Position::new(w60, 0));
test_position(size, -60.0, Position::new(0, w60));
test_position(size, -30.0, Position::new(0, w30));
test_position(size, 30.0, Position::new(w60, bb_width));
test_position(size, 60.0, Position::new(w30, bb_width));
test_position(size, 120.0, Position::new(bb_width, bb_width - w60));
test_position(size, 150.0, Position::new(bb_width, bb_width - w30));
}
#[test]
fn test_bounding_box_position_square_45_deg() {
let size = Size::new(100, 100);
let bb_width = (2.0f64 * size.width.0.powf(2.0)).sqrt();
test_position(size, -135.0, Position::new(bb_width / 2.0, 0));
test_position(size, -45.0, Position::new(0, bb_width / 2.0));
test_position(size, 45.0, Position::new(bb_width / 2.0, bb_width));
test_position(size, 135.0, Position::new(bb_width, bb_width / 2.0));
}
#[test]
fn test_bounding_box_position_square_90_deg() {
let size = Size::new(100, 100);
test_position(size, -180.0, Position::new(100, 0));
test_position(size, -90.0, Position::new(0, 0));
test_position(size, 0.0, Position::new(0, 100));
test_position(size, 90.0, Position::new(100, 100));
test_position(size, 180.0, Position::new(100, 0));
}
#[test]
fn test_bounding_box_size_rectangle_0_deg() {
let size = Size::new(200, 100);
for rotation in &[-180.0, 0.0, 180.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(Size, size, bounding_box_offset_and_size(&rotation, &size).1);
}
}
#[test]
fn test_bounding_box_size_rectangle_30_deg() {
let size = Size::new(200, 100);
let bb_width =
60.0f64.to_radians().sin() * size.width.0 + 30.0f64.to_radians().sin() * size.height.0;
let bb_height =
60.0f64.to_radians().sin() * size.height.0 + 30.0f64.to_radians().sin() * size.width.0;
let bb_size = Size::new(bb_width, bb_height);
for rotation in &[-150.0, -30.0, 30.0, 150.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(
Size,
bb_size,
bounding_box_offset_and_size(&rotation, &size).1
);
}
}
#[test]
fn test_bounding_box_size_rectangle_45_deg() {
let size = Size::new(200, 100);
let bb_width = 45.0f64.to_radians().sin() * (size.width.0 + size.height.0);
let bb_size = Size::new(bb_width, bb_width);
for rotation in &[-135.0, -45.0, 45.0, 135.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(
Size,
bb_size,
bounding_box_offset_and_size(&rotation, &size).1
);
}
}
#[test]
fn test_bounding_box_size_rectangle_60_deg() {
let size = Size::new(200, 100);
let bb_width =
30.0f64.to_radians().sin() * size.width.0 + 60.0f64.to_radians().sin() * size.height.0;
let bb_height =
30.0f64.to_radians().sin() * size.height.0 + 60.0f64.to_radians().sin() * size.width.0;
let bb_size = Size::new(bb_width, bb_height);
for rotation in &[-120.0, -60.0, 60.0, 120.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(
Size,
bb_size,
bounding_box_offset_and_size(&rotation, &size).1
);
}
}
#[test]
fn test_bounding_box_size_rectangle_90_deg() {
let size = Size::new(200, 100);
let bb_size = Size::new(100, 200);
for rotation in &[-90.0, 90.0] {
println!("rotation = {}", rotation);
let rotation = Rotation::from(*rotation);
assert_approx_eq!(
Size,
bb_size,
bounding_box_offset_and_size(&rotation, &size).1
);
}
}
#[test]
fn test_bounding_box_position_rectangle_30_deg() {
let size = Size::new(200, 100);
let bb_width =
30.0f64.to_radians().sin() * size.width.0 + 60.0f64.to_radians().sin() * size.height.0;
let bb_height =
30.0f64.to_radians().sin() * size.height.0 + 60.0f64.to_radians().sin() * size.width.0;
let h30 = 30.0f64.to_radians().cos() * size.height.0;
let h60 = 60.0f64.to_radians().cos() * size.height.0;
let w30 = 30.0f64.to_radians().cos() * size.width.0;
let w60 = 60.0f64.to_radians().cos() * size.width.0;
test_position(size, -150.0, Position::new(w30, 0));
test_position(size, -120.0, Position::new(w60, 0));
test_position(size, -60.0, Position::new(0, h60));
test_position(size, -30.0, Position::new(0, h30));
test_position(size, 30.0, Position::new(h60, bb_width));
test_position(size, 60.0, Position::new(h30, bb_height));
test_position(size, 120.0, Position::new(bb_width, bb_height - h60));
test_position(size, 150.0, Position::new(bb_height, bb_width - h30));
}
#[test]
fn test_bounding_box_position_rectangle_45_deg() {
let size = Size::new(200, 100);
let bb_width = 45.0f64.to_radians().sin() * (size.width.0 + size.height.0);
test_position(size, -135.0, Position::new(2.0 * bb_width / 3.0, 0));
test_position(size, -45.0, Position::new(0, bb_width / 3.0));
test_position(size, 45.0, Position::new(bb_width / 3.0, bb_width));
test_position(size, 135.0, Position::new(bb_width, 2.0 * bb_width / 3.0));
}
#[test]
fn test_bounding_box_position_rectangle_90_deg() {
let size = Size::new(200, 100);
test_position(size, -180.0, Position::new(200, 0));
test_position(size, -90.0, Position::new(0, 0));
test_position(size, 0.0, Position::new(0, 100));
test_position(size, 90.0, Position::new(100, 200));
test_position(size, 180.0, Position::new(200, 0));
}
}