1use std::fmt;
21use std::str::FromStr;
22
23pub const DPI: f32 = 96.0;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Unit {
29 Millimeter,
30 Centimeter,
31 Inch,
32 Point,
34 Pixel,
36}
37
38impl Unit {
39 pub fn symbol(self) -> &'static str {
40 match self {
41 Unit::Millimeter => "mm",
42 Unit::Centimeter => "cm",
43 Unit::Inch => "in",
44 Unit::Point => "pt",
45 Unit::Pixel => "px",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct Measure {
53 pub value: f32,
54 pub unit: Unit,
55}
56
57impl Measure {
58 pub const fn new(value: f32, unit: Unit) -> Self {
59 Self { value, unit }
60 }
61
62 pub const fn mm(value: f32) -> Self {
63 Self::new(value, Unit::Millimeter)
64 }
65
66 pub const fn cm(value: f32) -> Self {
67 Self::new(value, Unit::Centimeter)
68 }
69
70 pub const fn inches(value: f32) -> Self {
71 Self::new(value, Unit::Inch)
72 }
73
74 pub const fn pt(value: f32) -> Self {
75 Self::new(value, Unit::Point)
76 }
77
78 pub const fn px(value: f32) -> Self {
79 Self::new(value, Unit::Pixel)
80 }
81
82 pub fn to_px(self) -> f32 {
84 match self.unit {
85 Unit::Millimeter => self.value / 25.4 * DPI,
86 Unit::Centimeter => self.value / 2.54 * DPI,
87 Unit::Inch => self.value * DPI,
88 Unit::Point => self.value / 72.0 * DPI,
89 Unit::Pixel => self.value,
90 }
91 }
92
93 pub fn to_mm(self) -> f32 {
95 self.to_px() / DPI * 25.4
96 }
97}
98
99impl fmt::Display for Measure {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(f, "{}{}", self.value, self.unit.symbol())
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ParseError(pub String);
108
109impl fmt::Display for ParseError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.write_str(&self.0)
112 }
113}
114
115impl std::error::Error for ParseError {}
116
117fn split_number(text: &str) -> (&str, &str) {
118 let end = text
119 .char_indices()
120 .find(|(_, c)| !(c.is_ascii_digit() || *c == '.' || *c == ','))
121 .map(|(i, _)| i)
122 .unwrap_or(text.len());
123 (&text[..end], text[end..].trim())
124}
125
126fn parse_unit(text: &str) -> Option<Unit> {
127 match text.to_ascii_lowercase().as_str() {
128 "mm" => Some(Unit::Millimeter),
129 "cm" => Some(Unit::Centimeter),
130 "in" | "inch" | "inches" | "\"" => Some(Unit::Inch),
131 "pt" => Some(Unit::Point),
132 "px" => Some(Unit::Pixel),
133 _ => None,
134 }
135}
136
137impl FromStr for Measure {
138 type Err = ParseError;
139
140 fn from_str(text: &str) -> Result<Self, Self::Err> {
142 let text = text.trim();
143 let (number, unit) = split_number(text);
144 let value: f32 = number
145 .replace(',', ".")
146 .parse()
147 .map_err(|_| ParseError(format!("invalid number in `{text}`")))?;
148 let unit = parse_unit(unit)
149 .ok_or_else(|| ParseError(format!("missing or unknown unit in `{text}`")))?;
150 Ok(Self::new(value, unit))
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156pub enum IsoSeries {
157 A,
158 B,
159 C,
160}
161
162const ISO_A: [(f32, f32); 11] = [
163 (841.0, 1189.0),
164 (594.0, 841.0),
165 (420.0, 594.0),
166 (297.0, 420.0),
167 (210.0, 297.0),
168 (148.0, 210.0),
169 (105.0, 148.0),
170 (74.0, 105.0),
171 (52.0, 74.0),
172 (37.0, 52.0),
173 (26.0, 37.0),
174];
175
176const ISO_B: [(f32, f32); 11] = [
177 (1000.0, 1414.0),
178 (707.0, 1000.0),
179 (500.0, 707.0),
180 (353.0, 500.0),
181 (250.0, 353.0),
182 (176.0, 250.0),
183 (125.0, 176.0),
184 (88.0, 125.0),
185 (62.0, 88.0),
186 (44.0, 62.0),
187 (31.0, 44.0),
188];
189
190const ISO_C: [(f32, f32); 11] = [
191 (917.0, 1297.0),
192 (648.0, 917.0),
193 (458.0, 648.0),
194 (324.0, 458.0),
195 (229.0, 324.0),
196 (162.0, 229.0),
197 (114.0, 162.0),
198 (81.0, 114.0),
199 (57.0, 81.0),
200 (40.0, 57.0),
201 (28.0, 40.0),
202];
203
204#[derive(Debug, Clone, Copy, PartialEq)]
206pub enum PageSize {
207 Iso(IsoSeries, u8),
209 Letter,
211 Legal,
213 Tabloid,
215 Custom { width: Measure, height: Measure },
217 Fill,
220}
221
222impl PageSize {
223 pub const A3: PageSize = PageSize::Iso(IsoSeries::A, 3);
224 pub const A4: PageSize = PageSize::Iso(IsoSeries::A, 4);
225 pub const A5: PageSize = PageSize::Iso(IsoSeries::A, 5);
226 pub const A6: PageSize = PageSize::Iso(IsoSeries::A, 6);
227 pub const B4: PageSize = PageSize::Iso(IsoSeries::B, 4);
228 pub const B5: PageSize = PageSize::Iso(IsoSeries::B, 5);
229 pub const C5: PageSize = PageSize::Iso(IsoSeries::C, 5);
230
231 pub fn iso(series: IsoSeries, number: u8) -> Self {
233 PageSize::Iso(series, number.min(10))
234 }
235
236 pub fn custom(width: Measure, height: Measure) -> Self {
237 PageSize::Custom { width, height }
238 }
239
240 pub fn dimensions(&self) -> Option<(Measure, Measure)> {
242 match *self {
243 PageSize::Iso(series, number) => {
244 let table = match series {
245 IsoSeries::A => &ISO_A,
246 IsoSeries::B => &ISO_B,
247 IsoSeries::C => &ISO_C,
248 };
249 let (w, h) = table[number.min(10) as usize];
250 Some((Measure::mm(w), Measure::mm(h)))
251 }
252 PageSize::Letter => Some((Measure::inches(8.5), Measure::inches(11.0))),
253 PageSize::Legal => Some((Measure::inches(8.5), Measure::inches(14.0))),
254 PageSize::Tabloid => Some((Measure::inches(11.0), Measure::inches(17.0))),
255 PageSize::Custom { width, height } => Some((width, height)),
256 PageSize::Fill => None,
257 }
258 }
259}
260
261impl fmt::Display for PageSize {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 match self {
264 PageSize::Iso(series, number) => write!(f, "{series:?}{number}"),
265 PageSize::Letter => f.write_str("Letter"),
266 PageSize::Legal => f.write_str("Legal"),
267 PageSize::Tabloid => f.write_str("Tabloid"),
268 PageSize::Custom { width, height } => write!(f, "{width} x {height}"),
269 PageSize::Fill => f.write_str("Fill"),
270 }
271 }
272}
273
274impl FromStr for PageSize {
275 type Err = ParseError;
276
277 fn from_str(text: &str) -> Result<Self, Self::Err> {
281 let lower = text.trim().to_ascii_lowercase();
282
283 match lower.as_str() {
284 "letter" => return Ok(PageSize::Letter),
285 "legal" => return Ok(PageSize::Legal),
286 "tabloid" | "ledger" => return Ok(PageSize::Tabloid),
287 "fill" | "full" | "fullscreen" | "full screen" | "full width" => {
288 return Ok(PageSize::Fill);
289 }
290 _ => {}
291 }
292
293 let mut chars = lower.chars();
294 if let Some(first) = chars.next() {
295 let series = match first {
296 'a' => Some(IsoSeries::A),
297 'b' => Some(IsoSeries::B),
298 'c' => Some(IsoSeries::C),
299 _ => None,
300 };
301 if let Some(series) = series {
302 if let Ok(number) = chars.as_str().parse::<u8>() {
303 if number <= 10 {
304 return Ok(PageSize::Iso(series, number));
305 }
306 }
307 }
308 }
309
310 let parts: Vec<&str> = lower.split(['x', '×', '*']).map(str::trim).collect();
311 if parts.len() == 2 {
312 let height: Measure = parts[1].parse()?;
313 let width = match parts[0].parse::<Measure>() {
314 Ok(width) => width,
315 Err(_) => {
316 let value: f32 = parts[0]
317 .replace(',', ".")
318 .parse()
319 .map_err(|_| ParseError(format!("invalid width in `{text}`")))?;
320 Measure::new(value, height.unit)
321 }
322 };
323 return Ok(PageSize::Custom { width, height });
324 }
325
326 Err(ParseError(format!("unknown page size `{text}`")))
327 }
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
331pub enum Orientation {
332 #[default]
333 Portrait,
334 Landscape,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq)]
339pub struct Margins {
340 pub top: Measure,
341 pub right: Measure,
342 pub bottom: Measure,
343 pub left: Measure,
344}
345
346impl Margins {
347 pub fn new(top: Measure, right: Measure, bottom: Measure, left: Measure) -> Self {
348 Self {
349 top,
350 right,
351 bottom,
352 left,
353 }
354 }
355
356 pub fn all(margin: Measure) -> Self {
357 Self::new(margin, margin, margin, margin)
358 }
359
360 pub fn symmetric(vertical: Measure, horizontal: Measure) -> Self {
361 Self::new(vertical, horizontal, vertical, horizontal)
362 }
363}
364
365impl Default for Margins {
366 fn default() -> Self {
368 Self::all(Measure::cm(2.5))
369 }
370}
371
372#[derive(Debug, Clone, Copy, PartialEq)]
374pub struct Paper {
375 pub size: PageSize,
376 pub orientation: Orientation,
377 pub margins: Margins,
378}
379
380impl Default for Paper {
381 fn default() -> Self {
383 Self::new(PageSize::A4)
384 }
385}
386
387impl Paper {
388 pub fn new(size: PageSize) -> Self {
389 Self {
390 size,
391 orientation: Orientation::Portrait,
392 margins: Margins::default(),
393 }
394 }
395
396 pub fn a4() -> Self {
397 Self::new(PageSize::A4)
398 }
399
400 pub fn letter() -> Self {
401 Self::new(PageSize::Letter).with_margins(Margins::all(Measure::inches(1.0)))
402 }
403
404 pub fn fill() -> Self {
406 Self::new(PageSize::Fill)
407 }
408
409 pub fn with_size(mut self, size: PageSize) -> Self {
410 self.size = size;
411 self
412 }
413
414 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
415 self.orientation = orientation;
416 self
417 }
418
419 pub fn landscape(self) -> Self {
420 self.with_orientation(Orientation::Landscape)
421 }
422
423 pub fn portrait(self) -> Self {
424 self.with_orientation(Orientation::Portrait)
425 }
426
427 pub fn with_margins(mut self, margins: Margins) -> Self {
428 self.margins = margins;
429 self
430 }
431
432 pub fn is_fill(&self) -> bool {
433 self.size == PageSize::Fill
434 }
435
436 pub fn size_px(&self) -> Option<(f32, f32)> {
439 let (w, h) = self.size.dimensions()?;
440 let (w, h) = (w.to_px(), h.to_px());
441 let (short, long) = (w.min(h), w.max(h));
442 Some(match self.orientation {
443 Orientation::Portrait => (short, long),
444 Orientation::Landscape => (long, short),
445 })
446 }
447}