ffcine_reframe/
profile.rs1use std::str::FromStr;
6
7use ffcine_core::Error;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct Profile {
12 pub name: String,
14 pub width: u32,
15 pub height: u32,
16 pub max_punch: f64,
18 pub safe_area: Option<SafeArea>,
20}
21
22pub const DEFAULT_MAX_PUNCH: f64 = 1.4;
23
24impl Profile {
25 pub fn new(name: impl Into<String>, width: u32, height: u32) -> Self {
26 Self {
27 name: name.into(),
28 width,
29 height,
30 max_punch: DEFAULT_MAX_PUNCH,
31 safe_area: None,
32 }
33 }
34
35 pub fn preset(name: &str) -> Option<Self> {
38 let (n, w, h) = match name {
39 "landscape" | "16:9" => ("landscape", 3840, 2160),
40 "hd" => ("hd", 1920, 1080),
41 "reel" | "9:16" => ("reel", 1080, 1920),
42 "feed" | "4:5" => ("feed", 1080, 1350),
43 "square" | "1:1" => ("square", 1080, 1080),
44 _ => return None,
45 };
46 Some(Self::new(n, w, h))
47 }
48
49 pub fn aspect(&self) -> f64 {
50 f64::from(self.width) / f64::from(self.height)
51 }
52
53 pub fn default_safe_area(&self) -> SafeArea {
56 let ar = self.aspect();
57 if (ar - 9.0 / 16.0).abs() < 0.01 {
58 SafeArea::REELS
59 } else if (ar - 4.0 / 5.0).abs() < 0.01 {
60 SafeArea::FEED
61 } else {
62 SafeArea::TITLE
63 }
64 }
65}
66
67impl FromStr for Profile {
68 type Err = Error;
69 fn from_str(s: &str) -> Result<Self, Error> {
71 if let Some(p) = Self::preset(s) {
72 return Ok(p);
73 }
74 let invalid = || Error::Invalid {
75 what: "profile (preset, WxH or name=WxH)",
76 value: s.to_owned(),
77 };
78 let (name, size) = s.split_once('=').unwrap_or((s, s));
79 let (w, h) = size.split_once('x').ok_or_else(invalid)?;
80 let (w, h): (u32, u32) = (
81 w.parse().map_err(|_| invalid())?,
82 h.parse().map_err(|_| invalid())?,
83 );
84 if w == 0 || h == 0 {
85 return Err(invalid());
86 }
87 Ok(Self::new(name, w, h))
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
94pub struct SafeArea {
95 pub top: f64,
96 pub right: f64,
97 pub bottom: f64,
98 pub left: f64,
99}
100
101impl SafeArea {
102 pub const REELS: Self = Self {
105 top: 0.115,
106 right: 0.11,
107 bottom: 0.22,
108 left: 0.055,
109 };
110 pub const FEED: Self = Self {
112 top: 0.05,
113 right: 0.0625,
114 bottom: 0.05,
115 left: 0.0625,
116 };
117 pub const TITLE: Self = Self {
119 top: 0.05,
120 right: 0.05,
121 bottom: 0.05,
122 left: 0.05,
123 };
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn parse_profiles() {
132 let p: Profile = "9:16".parse().unwrap();
133 assert_eq!((p.name.as_str(), p.width, p.height), ("reel", 1080, 1920));
134 assert_eq!(p.default_safe_area(), SafeArea::REELS);
135 let p: Profile = "story=720x1280".parse().unwrap();
136 assert_eq!((p.name.as_str(), p.width), ("story", 720));
137 assert_eq!(p.default_safe_area(), SafeArea::REELS);
138 assert_eq!(
139 "1080x1350".parse::<Profile>().unwrap().default_safe_area(),
140 SafeArea::FEED
141 );
142 assert!("reels".parse::<Profile>().is_err());
143 assert!("0x10".parse::<Profile>().is_err());
144 }
145}