use std::path::{Path, PathBuf};
use super::{Editor, Selection};
use crate::geometry::Point;
use crate::model::{Canvas, CanvasSize, Step};
use crate::project::Project;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Preset {
FullHd,
UltraHd,
A4,
Letter,
}
impl Preset {
pub const ALL: [Preset; 4] = [Preset::FullHd, Preset::UltraHd, Preset::A4, Preset::Letter];
pub fn label(self) -> &'static str {
match self {
Preset::FullHd => "1920 × 1080",
Preset::UltraHd => "3840 × 2160",
Preset::A4 => "A4 at 300 DPI",
Preset::Letter => "US Letter at 300 DPI",
}
}
pub fn size(self) -> (u32, u32) {
match self {
Preset::FullHd => (1920, 1080),
Preset::UltraHd => (3840, 2160),
Preset::A4 => (2480, 3508),
Preset::Letter => (2550, 3300),
}
}
pub fn dpi(self) -> Option<f64> {
match self {
Preset::A4 | Preset::Letter => Some(300.0),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CanvasChoice {
SideBySide,
Stacked,
Largest,
Preset(Preset),
Custom {
width: u32,
height: u32,
dpi: Option<f64>,
},
}
impl CanvasChoice {
pub fn label(&self) -> String {
match self {
CanvasChoice::SideBySide => "Side by side".to_owned(),
CanvasChoice::Stacked => "Stacked".to_owned(),
CanvasChoice::Largest => "Size of the largest image".to_owned(),
CanvasChoice::Preset(p) => p.label().to_owned(),
CanvasChoice::Custom { .. } => "Custom".to_owned(),
}
}
pub fn layout(&self, sizes: &[(u32, u32)]) -> (Canvas, Vec<(i32, i32)>) {
let max_w = sizes.iter().map(|s| s.0).max().unwrap_or(1);
let max_h = sizes.iter().map(|s| s.1).max().unwrap_or(1);
let fixed = |width: u32, height: u32, dpi: Option<f64>| Canvas {
size: CanvasSize::Fixed {
width: width.max(1),
height: height.max(1),
},
dpi,
};
let centered = |cw: u32, ch: u32| -> Vec<(i32, i32)> {
sizes
.iter()
.map(|&(w, h)| (center(cw, w), center(ch, h)))
.collect()
};
match *self {
CanvasChoice::SideBySide => {
let mut x = 0i32;
let positions = sizes
.iter()
.map(|&(w, h)| {
let p = (x, center(max_h, h));
x = x.saturating_add_unsigned(w);
p
})
.collect();
(
fixed(
sizes.iter().fold(0, |sum, s| sum.saturating_add(s.0)),
max_h,
None,
),
positions,
)
}
CanvasChoice::Stacked => {
let mut y = 0i32;
let positions = sizes
.iter()
.map(|&(w, h)| {
let p = (center(max_w, w), y);
y = y.saturating_add_unsigned(h);
p
})
.collect();
(
fixed(
max_w,
sizes.iter().fold(0, |sum, s| sum.saturating_add(s.1)),
None,
),
positions,
)
}
CanvasChoice::Largest => {
let &(w, h) = sizes
.iter()
.max_by_key(|(w, h)| u64::from(*w).saturating_mul(u64::from(*h)))
.unwrap_or(&(1, 1));
(fixed(w, h, None), centered(w, h))
}
CanvasChoice::Preset(preset) => {
let (w, h) = preset.size();
(fixed(w, h, preset.dpi()), centered(w, h))
}
CanvasChoice::Custom { width, height, dpi } => {
(fixed(width, height, dpi), centered(width, height))
}
}
}
}
fn center(outer: u32, inner: u32) -> i32 {
(i64::from(outer).saturating_sub(i64::from(inner)) / 2) as i32
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrintUnit {
Inches,
Centimeters,
}
impl PrintUnit {
pub fn label(self) -> &'static str {
match self {
PrintUnit::Inches => "in",
PrintUnit::Centimeters => "cm",
}
}
}
pub fn pixels_for_print_size(size: f64, unit: PrintUnit, dpi: f64) -> u32 {
let inches = match unit {
PrintUnit::Inches => size,
PrintUnit::Centimeters => size / 2.54,
};
((inches * dpi).round() as u32).max(1)
}
pub fn proportional(original: u32, original_other: u32, new: u32) -> u32 {
((f64::from(new) * f64::from(original_other) / f64::from(original)).round() as u32).max(1)
}
impl Editor {
fn image_size(&mut self, path: &Path) -> Result<(u32, u32), String> {
let source = self
.renderer
.source(path)
.map_err(|problem| match problem {
crate::render::SourceProblem::Missing => {
format!("{} does not exist", path.display())
}
crate::render::SourceProblem::Unreadable(e) => format!("{}: {e}", path.display()),
})?;
Ok(source.pixels.dimensions())
}
pub fn new_composition(
&mut self,
paths: &[PathBuf],
choice: Option<CanvasChoice>,
) -> Result<(), String> {
let paths: Vec<PathBuf> = paths
.iter()
.map(|p| std::path::absolute(p).map_err(|e| e.to_string()))
.collect::<Result<_, _>>()?;
let sizes = paths
.iter()
.map(|p| self.image_size(p))
.collect::<Result<Vec<_>, _>>()?;
let (canvas, positions) = match choice {
Some(choice) => choice.layout(&sizes),
None => (Canvas::default(), vec![(0, 0); sizes.len()]),
};
let mut project = Project::with_canvas(canvas);
for ((path, &(w, h)), &(x, y)) in paths.iter().zip(&sizes).zip(&positions) {
let source = project.add_source(path.clone());
let id = project.state().next_item_id();
project
.apply(Step::AddItem {
id,
source,
x,
y,
source_width: w,
source_height: h,
})
.map_err(|e| e.to_string())?;
}
project.mark_saved();
self.replace_project(project, None);
if let Some(path) = paths.last() {
self.remember_folder(path);
}
Ok(())
}
pub fn add_images(&mut self, paths: &[PathBuf], at: Option<Point>) -> Result<(), String> {
if self.is_empty() && self.path.is_none() && !self.project.is_dirty() {
return self.open_images(paths);
}
for path in paths {
let path = std::path::absolute(path).map_err(|e| e.to_string())?;
let (w, h) = self.image_size(&path)?;
let state = self.project.state();
let canvas = state.canvas_rect();
let (x, y) = match (at, state.canvas.size) {
(Some(screen), _) => {
let p = self.view.to_canvas(screen);
(
(p.x - f64::from(w) / 2.0).round() as i32,
(p.y - f64::from(h) / 2.0).round() as i32,
)
}
(None, CanvasSize::Fit) => (canvas.right() as i32, canvas.y as i32),
(None, CanvasSize::Fixed { width, height }) => {
(center(width, w), center(height, h))
}
};
self.remember_folder(&path);
let source = self.project.add_source(path);
let id = self.project.state().next_item_id();
if !self.record(Step::AddItem {
id,
source,
x,
y,
source_width: w,
source_height: h,
}) {
return Err(self.message.take().unwrap_or_default());
}
self.selection = Selection::Item(id);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn print_size() {
assert_eq!(pixels_for_print_size(4.0, PrintUnit::Inches, 300.0), 1200);
assert_eq!(
pixels_for_print_size(2.54, PrintUnit::Centimeters, 300.0),
300
);
}
#[test]
fn side_by_side_layout() {
let (canvas, positions) = CanvasChoice::SideBySide.layout(&[(100, 50), (40, 80)]);
assert_eq!(
canvas.size,
CanvasSize::Fixed {
width: 140,
height: 80
}
);
assert_eq!(positions, vec![(0, 15), (100, 0)]);
}
}