1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16
17use crate::{ErrorCode, FileMakerError, Result, Size, Unit};
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum Orientation {
23 #[default]
25 Portrait,
26 Landscape,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct Preset {
34 pub name: String,
36 pub version: u16,
38 pub size: Size,
40}
41
42impl Preset {
43 #[must_use]
45 pub fn oriented_size(&self, orientation: Orientation) -> Size {
46 match orientation {
47 Orientation::Portrait => self.size,
48 Orientation::Landscape => Size {
49 width: self.size.height,
50 height: self.size.width,
51 },
52 }
53 }
54}
55
56#[derive(Clone, Debug)]
58pub struct PresetRegistry {
59 presets: BTreeMap<String, Preset>,
60}
61
62impl Default for PresetRegistry {
63 fn default() -> Self {
64 Self::v1().expect("built-in preset constants are valid")
66 }
67}
68
69impl PresetRegistry {
70 pub fn v1() -> Result<Self> {
72 let mut registry = Self {
73 presets: BTreeMap::new(),
74 };
75 for (name, width, height, unit) in BUILT_INS {
76 let size = match *unit {
77 "mm" => Size::new(mm(*width)?, mm(*height)?)?,
78 "in" => Size::new(inches(*width)?, inches(*height)?)?,
79 "mil" => Size::new(milli_inches(*width)?, milli_inches(*height)?)?,
80 "px" => Size::new(px(*width)?, px(*height)?)?,
81 _ => return Err(invalid_preset("invalid built-in preset unit")),
82 };
83 registry.register(Preset {
84 name: (*name).to_owned(),
85 version: 1,
86 size,
87 })?;
88 }
89 Ok(registry)
90 }
91
92 pub fn register(&mut self, preset: Preset) -> Result<()> {
94 if !valid_name(&preset.name) || preset.version == 0 {
95 return Err(invalid_preset("invalid preset name or version"));
96 }
97 if self.presets.contains_key(&preset.name) {
98 return Err(invalid_preset(format!(
99 "duplicate preset `{}`",
100 preset.name
101 )));
102 }
103 self.presets.insert(preset.name.clone(), preset);
104 Ok(())
105 }
106
107 pub fn get(&self, name: &str) -> Result<&Preset> {
109 self.presets
110 .get(name)
111 .ok_or_else(|| invalid_preset(format!("unknown preset `{name}`")))
112 }
113
114 pub fn iter(&self) -> impl Iterator<Item = (&str, &Preset)> {
116 self.presets
117 .iter()
118 .map(|(name, preset)| (name.as_str(), preset))
119 }
120}
121
122fn valid_name(name: &str) -> bool {
123 !name.is_empty()
124 && name.len() <= 64
125 && name
126 .bytes()
127 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
128}
129
130fn mm(value: i64) -> Result<Unit> {
131 Unit::from_ratio(i128::from(value) * 360, 127)
132}
133
134fn inches(value_hundredths: i64) -> Result<Unit> {
135 Unit::from_ratio(i128::from(value_hundredths) * 72, 100)
136}
137
138fn milli_inches(value_thousandths: i64) -> Result<Unit> {
139 Unit::from_ratio(i128::from(value_thousandths) * 72, 1_000)
140}
141
142fn px(value: i64) -> Result<Unit> {
143 Unit::from_ratio(i128::from(value) * 3, 4)
144}
145
146fn invalid_preset(message: impl Into<String>) -> FileMakerError {
147 FileMakerError::new(ErrorCode::GeometryInvalid, message)
148}
149
150const BUILT_INS: &[(&str, i64, i64, &str)] = &[
151 ("A0", 841, 1189, "mm"),
152 ("A1", 594, 841, "mm"),
153 ("A2", 420, 594, "mm"),
154 ("A3", 297, 420, "mm"),
155 ("A4", 210, 297, "mm"),
156 ("A5", 148, 210, "mm"),
157 ("A6", 105, 148, "mm"),
158 ("A7", 74, 105, "mm"),
159 ("A8", 52, 74, "mm"),
160 ("A9", 37, 52, "mm"),
161 ("A10", 26, 37, "mm"),
162 ("B0", 1000, 1414, "mm"),
163 ("B1", 707, 1000, "mm"),
164 ("B2", 500, 707, "mm"),
165 ("B3", 353, 500, "mm"),
166 ("B4", 250, 353, "mm"),
167 ("B5", 176, 250, "mm"),
168 ("B6", 125, 176, "mm"),
169 ("B7", 88, 125, "mm"),
170 ("B8", 62, 88, "mm"),
171 ("B9", 44, 62, "mm"),
172 ("B10", 31, 44, "mm"),
173 ("C0", 917, 1297, "mm"),
174 ("C1", 648, 917, "mm"),
175 ("C2", 458, 648, "mm"),
176 ("C3", 324, 458, "mm"),
177 ("C4", 229, 324, "mm"),
178 ("C5", 162, 229, "mm"),
179 ("C6", 114, 162, "mm"),
180 ("C7", 81, 114, "mm"),
181 ("C8", 57, 81, "mm"),
182 ("C9", 40, 57, "mm"),
183 ("C10", 28, 40, "mm"),
184 ("Letter", 850, 1100, "in"),
185 ("Legal", 850, 1400, "in"),
186 ("Tabloid", 1100, 1700, "in"),
187 ("Ledger", 1700, 1100, "in"),
188 ("Executive", 725, 1050, "in"),
189 ("HD", 1280, 720, "px"),
190 ("FHD", 1920, 1080, "px"),
191 ("QHD", 2560, 1440, "px"),
192 ("UHD", 3840, 2160, "px"),
193 ("5K", 5120, 2880, "px"),
194 ("8K", 7680, 4320, "px"),
195 ("Ratio1x1", 1000, 1000, "px"),
196 ("Ratio4x3", 1200, 900, "px"),
197 ("Ratio3x2", 1200, 800, "px"),
198 ("Ratio16x9", 1600, 900, "px"),
199 ("Ratio21x9", 2100, 900, "px"),
200 ("Photo4x6", 400, 600, "in"),
201 ("Photo5x7", 500, 700, "in"),
202 ("Photo8x10", 800, 1000, "in"),
203 ("Photo10x15cm", 100, 150, "mm"),
204 ("EnvelopeDL", 110, 220, "mm"),
205 ("EnvelopeC5", 162, 229, "mm"),
206 ("EnvelopeC6", 114, 162, "mm"),
207 ("EnvelopeMonarch", 3875, 7500, "mil"),
208 ("Label4x6", 400, 600, "in"),
209 ("LabelAvery5160", 2625, 1000, "mil"),
210 ("Thermal58", 58, 200, "mm"),
211 ("Thermal80", 80, 200, "mm"),
212 ("InstagramPost-v1", 1080, 1080, "px"),
213 ("InstagramStory-v1", 1080, 1920, "px"),
214 ("LinkedInPost-v1", 1200, 627, "px"),
215];
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn orientation_is_separate_from_preset() {
223 let registry = PresetRegistry::v1().unwrap();
224 let a4 = registry.get("A4").unwrap();
225 let landscape = a4.oriented_size(Orientation::Landscape);
226 assert_eq!(landscape.width, a4.size.height);
227 assert_eq!(landscape.height, a4.size.width);
228 }
229
230 #[test]
231 fn v1_contains_all_declared_preset_families() {
232 let registry = PresetRegistry::v1().unwrap();
233 for name in [
234 "A10",
235 "B0",
236 "B10",
237 "C0",
238 "C10",
239 "Ratio16x9",
240 "Photo4x6",
241 "EnvelopeDL",
242 "LabelAvery5160",
243 "Thermal80",
244 "InstagramStory-v1",
245 ] {
246 assert_eq!(registry.get(name).unwrap().version, 1);
247 }
248 }
249}