1#![warn(missing_docs)]
29use std::{
30 borrow::Cow,
31 error::Error,
32 fmt::Display,
33 fs::File,
34 io::{BufRead, BufReader, ErrorKind},
35 ops::Not,
36 path::{Path, PathBuf},
37 str::FromStr,
38};
39
40use xdg::BaseDirectories;
41
42#[cfg(target_os = "linux")]
43mod locale;
44
45#[cfg(feature = "serde")]
46mod serde;
47
48include!(concat!(env!("OUT_DIR"), "/paperspecs.rs"));
49
50static PAPERSIZE_FILENAME: &str = "papersize";
51static PAPERSPECS_FILENAME: &str = "paperspecs";
52
53enum DefaultPaper {
54 Name(String),
55 Size(PaperSize),
56}
57
58#[derive(Copy, Clone, Debug, PartialEq, Eq)]
60pub enum Unit {
61 Point,
63
64 Inch,
66
67 Millimeter,
69}
70
71#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73pub struct ParseUnitError;
74
75impl Error for ParseUnitError {}
76
77impl Display for ParseUnitError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "unknown unit")
80 }
81}
82
83impl FromStr for Unit {
84 type Err = ParseUnitError;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 match s {
90 "pt" => Ok(Self::Point),
91 "in" => Ok(Self::Inch),
92 "mm" => Ok(Self::Millimeter),
93 _ => Err(ParseUnitError),
94 }
95 }
96}
97
98impl Unit {
99 pub fn name(&self) -> &'static str {
101 match self {
102 Unit::Point => "pt",
103 Unit::Inch => "in",
104 Unit::Millimeter => "mm",
105 }
106 }
107
108 pub fn as_unit(&self, other: Unit) -> f64 {
113 match (*self, other) {
114 (Unit::Point, Unit::Point) => 1.0,
115 (Unit::Point, Unit::Inch) => 1.0 / 72.0,
116 (Unit::Point, Unit::Millimeter) => 25.4 / 72.0,
117 (Unit::Inch, Unit::Point) => 72.0,
118 (Unit::Inch, Unit::Inch) => 1.0,
119 (Unit::Inch, Unit::Millimeter) => 25.4,
120 (Unit::Millimeter, Unit::Point) => 72.0 / 25.4,
121 (Unit::Millimeter, Unit::Inch) => 1.0 / 25.4,
122 (Unit::Millimeter, Unit::Millimeter) => 1.0,
123 }
124 }
125}
126
127impl Display for Unit {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write!(f, "{}", self.name())
130 }
131}
132
133#[derive(Copy, Clone, Debug, PartialEq)]
135pub struct Length {
136 pub value: f64,
138
139 pub unit: Unit,
141}
142
143impl Length {
144 pub fn new(value: f64, unit: Unit) -> Self {
146 Self { value, unit }
147 }
148
149 pub fn as_unit(&self, unit: Unit) -> Self {
151 Self {
152 value: self.value * self.unit.as_unit(unit),
153 unit,
154 }
155 }
156
157 pub fn into_unit(&self, unit: Unit) -> f64 {
159 self.as_unit(unit).value
160 }
161}
162
163#[derive(Copy, Clone, Debug)]
165pub enum ParseLengthError {
166 MissingUnit,
168 InvalidUnit,
170 InvalidValue,
172}
173
174impl Error for ParseLengthError {}
175
176impl Display for ParseLengthError {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 match self {
179 ParseLengthError::MissingUnit => write!(f, "Missing unit"),
180 ParseLengthError::InvalidUnit => write!(f, "Invalid unit of measurement"),
181 ParseLengthError::InvalidValue => write!(f, "Invalid length"),
182 }
183 }
184}
185
186impl FromStr for Length {
187 type Err = ParseLengthError;
188
189 fn from_str(s: &str) -> Result<Self, Self::Err> {
190 if let Some(index) = s.find(|c: char| c.is_alphabetic()) {
191 let (value, unit) = s.split_at(index);
192 let value = value.parse().map_err(|_| ParseLengthError::InvalidValue)?;
193 let unit = unit.parse().map_err(|_| ParseLengthError::InvalidUnit)?;
194 Ok(Self { value, unit })
195 } else {
196 Err(ParseLengthError::MissingUnit)
197 }
198 }
199}
200
201impl Display for Length {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 write!(f, "{}{}", self.value, self.unit)
204 }
205}
206
207#[derive(Copy, Clone, Debug, PartialEq)]
209pub struct PaperSize {
210 pub width: f64,
212
213 pub height: f64,
215
216 pub unit: Unit,
218}
219
220impl Default for PaperSize {
221 fn default() -> Self {
223 Self::new(210.0, 297.0, Unit::Millimeter)
224 }
225}
226
227impl PaperSize {
228 pub fn new(width: f64, height: f64, unit: Unit) -> Self {
230 Self {
231 width,
232 height,
233 unit,
234 }
235 }
236
237 pub fn as_unit(&self, unit: Unit) -> PaperSize {
239 Self {
240 width: self.width * self.unit.as_unit(unit),
241 height: self.height * self.unit.as_unit(unit),
242 unit,
243 }
244 }
245
246 pub fn into_width_height(self) -> (f64, f64) {
248 (self.width, self.height)
249 }
250
251 pub fn eq_rounded(&self, other: &Self, unit: Unit) -> bool {
254 let (aw, ah) = self.as_unit(unit).into_width_height();
255 let (bw, bh) = other.as_unit(unit).into_width_height();
256 aw.round() == bw.round() && ah.round() == bh.round()
257 }
258
259 pub fn width(&self) -> Length {
261 Length::new(self.width, self.unit)
262 }
263
264 pub fn height(&self) -> Length {
266 Length::new(self.height, self.unit)
267 }
268}
269
270#[derive(Copy, Clone, Debug, PartialEq, Eq)]
272pub enum ParsePaperSizeError {
273 InvalidHeight,
275
276 InvalidWidth,
278
279 InvalidUnit,
281
282 MissingUnit,
284
285 MissingDelimiter,
287}
288
289impl Error for ParsePaperSizeError {}
290
291impl Display for ParsePaperSizeError {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 match self {
294 ParsePaperSizeError::InvalidHeight => write!(f, "Invalid paper height"),
295 ParsePaperSizeError::InvalidWidth => write!(f, "Invalid paper width"),
296 ParsePaperSizeError::InvalidUnit => write!(f, "Invalid unit of measurement"),
297 ParsePaperSizeError::MissingUnit => write!(f, "Missing unit in paper size"),
298 ParsePaperSizeError::MissingDelimiter => write!(f, "Missing delimiter in paper size"),
299 }
300 }
301}
302
303impl FromStr for PaperSize {
304 type Err = ParsePaperSizeError;
305
306 fn from_str(s: &str) -> Result<Self, Self::Err> {
309 let Some((width, rest)) = s.split_once([',', 'x']) else {
310 return Err(ParsePaperSizeError::MissingDelimiter);
311 };
312 let (height, unit) = if let Some(result) = rest.split_once(',') {
313 result
314 } else if let Some(alpha) = rest.find(|c: char| c.is_alphabetic()) {
315 rest.split_at(alpha)
316 } else {
317 return Err(ParsePaperSizeError::MissingUnit);
318 };
319
320 let width = f64::from_str(width.trim()).map_err(|_| ParsePaperSizeError::InvalidWidth)?;
321 let height =
322 f64::from_str(height.trim()).map_err(|_| ParsePaperSizeError::InvalidHeight)?;
323 let unit = Unit::from_str(unit.trim()).map_err(|_| ParsePaperSizeError::InvalidUnit)?;
324 Ok(Self {
325 width,
326 height,
327 unit,
328 })
329 }
330}
331
332impl Display for PaperSize {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 write!(f, "{}x{}{}", self.width, self.height, self.unit)
335 }
336}
337
338#[derive(Copy, Clone, Debug, PartialEq, Eq)]
340pub enum ParsePaperSpecError {
341 InvalidHeight,
343
344 InvalidWidth,
346
347 InvalidUnit,
349
350 MissingField,
352}
353
354impl From<ParsePaperSizeError> for ParsePaperSpecError {
355 fn from(value: ParsePaperSizeError) -> Self {
356 match value {
357 ParsePaperSizeError::InvalidHeight => Self::InvalidHeight,
358 ParsePaperSizeError::InvalidWidth => Self::InvalidWidth,
359 ParsePaperSizeError::InvalidUnit => Self::InvalidUnit,
360 ParsePaperSizeError::MissingUnit => Self::MissingField,
361 ParsePaperSizeError::MissingDelimiter => Self::MissingField,
362 }
363 }
364}
365
366impl Error for ParsePaperSpecError {}
367
368impl Display for ParsePaperSpecError {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 match self {
371 ParsePaperSpecError::InvalidHeight => write!(f, "Invalid paper height."),
372 ParsePaperSpecError::InvalidWidth => write!(f, "Invalid paper width."),
373 ParsePaperSpecError::InvalidUnit => write!(f, "Invalid unit of measurement."),
374 ParsePaperSpecError::MissingField => write!(f, "Missing field in paper specification."),
375 }
376 }
377}
378
379#[derive(Clone, Debug, PartialEq)]
381pub struct PaperSpec {
382 pub name: Cow<'static, str>,
384
385 pub size: PaperSize,
387}
388
389impl PaperSpec {
390 pub fn new(name: impl Into<Cow<'static, str>>, size: PaperSize) -> Self {
392 Self {
393 name: name.into(),
394 size,
395 }
396 }
397}
398
399impl FromStr for PaperSpec {
400 type Err = ParsePaperSpecError;
401
402 fn from_str(s: &str) -> Result<Self, Self::Err> {
407 let (name, size) = s.split_once(',').ok_or(ParsePaperSpecError::MissingField)?;
408 Ok(Self {
409 name: String::from(name).into(),
410 size: size.parse()?,
411 })
412 }
413}
414
415#[derive(Debug)]
417pub enum CatalogBuildError {
418 ParseError {
420 path: PathBuf,
422
423 line_number: usize,
425
426 error: ParsePaperSpecError,
428 },
429
430 IoError {
432 path: PathBuf,
434
435 error: std::io::Error,
437 },
438}
439
440impl Error for CatalogBuildError {}
441
442impl Display for CatalogBuildError {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 match self {
445 CatalogBuildError::ParseError {
446 path,
447 line_number,
448 error,
449 } => write!(f, "{}:{line_number}: {error}", path.display()),
450 CatalogBuildError::IoError { path, error } => {
451 write!(f, "{}: {error}", path.display())
452 }
453 }
454 }
455}
456
457pub struct CatalogBuilder<'a> {
463 papersize: Option<Option<&'a str>>,
464 use_locale: bool,
465 user_config_dir: Option<Option<&'a Path>>,
466 system_config_dir: Option<&'a Path>,
467 error_cb: Box<dyn FnMut(CatalogBuildError) + 'a>,
468}
469
470impl<'a> Default for CatalogBuilder<'a> {
471 fn default() -> Self {
472 Self {
473 use_locale: true,
474 papersize: None,
475 user_config_dir: None,
476 system_config_dir: Some(Path::new("/etc")),
477 error_cb: Box::new(drop),
478 }
479 }
480}
481
482fn fallback_specs() -> (Vec<PaperSpec>, PaperSpec) {
483 let specs = STANDARD_PAPERSPECS.into_iter().cloned().collect::<Vec<_>>();
484 let default = specs.first().unwrap().clone();
485 (specs, default)
486}
487
488fn read_specs<E>(
489 user_config_dir: Option<&Path>,
490 system_config_dir: Option<&Path>,
491 mut error_cb: E,
492) -> Option<(Vec<PaperSpec>, PaperSpec)>
493where
494 E: FnMut(CatalogBuildError),
495{
496 fn read_paperspecs_file(
497 directory: Option<&Path>,
498 error_cb: &mut dyn FnMut(CatalogBuildError),
499 ) -> Vec<PaperSpec> {
500 let mut specs = Vec::new();
501 if let Some(directory) = directory {
502 let path = directory.join(PAPERSPECS_FILENAME);
503 match File::open(&path) {
504 Ok(file) => {
505 let reader = BufReader::new(file);
506 for (line, line_number) in reader.lines().zip(1..) {
507 match line
508 .map_err(|error| CatalogBuildError::IoError {
509 path: path.clone(),
510 error,
511 })
512 .and_then(|line| {
513 PaperSpec::from_str(&line).map_err(|error| {
514 CatalogBuildError::ParseError {
515 path: path.clone(),
516 line_number,
517 error,
518 }
519 })
520 }) {
521 Ok(spec) => specs.push(spec),
522 Err(error) => error_cb(error),
523 }
524 }
525 }
526 Err(error) if error.kind() == ErrorKind::NotFound => (),
527 Err(error) => error_cb(CatalogBuildError::IoError { path, error }),
528 }
529 }
530 specs
531 }
532
533 let user_specs = read_paperspecs_file(user_config_dir, &mut error_cb);
534 let system_specs = read_paperspecs_file(system_config_dir, &mut error_cb);
535 let default_spec = system_specs.first().or(user_specs.first())?.clone();
536 Some((
537 user_specs.into_iter().chain(system_specs).collect(),
538 default_spec,
539 ))
540}
541
542fn default_paper<E>(
543 papersize: Option<Option<&str>>,
544 user_config_dir: Option<&Path>,
545 _use_locale: bool,
546 system_config_dir: Option<&Path>,
547 default: &PaperSpec,
548 mut error_cb: E,
549) -> DefaultPaper
550where
551 E: FnMut(CatalogBuildError),
552{
553 fn read_papersize_file<P, E>(path: P, mut error_cb: E) -> Option<String>
554 where
555 P: AsRef<Path>,
556 E: FnMut(CatalogBuildError),
557 {
558 fn inner(path: &Path) -> std::io::Result<Option<String>> {
559 let file = BufReader::new(File::open(path)?);
560 let line = file.lines().next().unwrap_or(Ok(String::new()))?;
561 let name = line.split(',').next().unwrap_or("");
562 Ok(name.is_empty().not().then(|| name.into()))
563 }
564 let path = path.as_ref();
565 match inner(path) {
566 Ok(result) => result,
567 Err(error) => {
568 if error.kind() != ErrorKind::NotFound {
569 error_cb(CatalogBuildError::IoError {
570 path: path.to_path_buf(),
571 error,
572 });
573 }
574 None
575 }
576 }
577 }
578
579 let env_var;
581 let paper_name = match papersize {
582 Some(paper_name) => paper_name,
583 None => {
584 env_var = std::env::var("PAPERSIZE").ok();
585 env_var.as_deref()
586 }
587 };
588 if let Some(paper_name) = paper_name
589 && !paper_name.is_empty()
590 {
591 return DefaultPaper::Name(paper_name.into());
592 }
593
594 if let Some(dir) = user_config_dir
596 && let path = dir.join(PAPERSIZE_FILENAME)
597 && let Some(paper_name) = read_papersize_file(path, &mut error_cb)
598 {
599 return DefaultPaper::Name(paper_name);
600 }
601
602 #[cfg(target_os = "linux")]
604 if _use_locale && let Some(paper_size) = locale::locale_paper_size() {
605 return DefaultPaper::Size(paper_size);
606 }
607
608 if let Some(system_config_dir) = system_config_dir
609 && let Some(paper_name) =
610 read_papersize_file(system_config_dir.join(PAPERSIZE_FILENAME), &mut error_cb)
611 {
612 return DefaultPaper::Name(paper_name);
613 }
614
615 DefaultPaper::Name(default.name.as_ref().into())
617}
618
619impl<'a> CatalogBuilder<'a> {
620 pub fn new() -> Self {
622 Self::default()
623 }
624
625 pub fn build(self) -> Catalog {
634 self.build_inner(|user_config_dir, system_config_dir, error_cb| {
635 Some(
636 read_specs(user_config_dir, system_config_dir, error_cb)
637 .unwrap_or_else(fallback_specs),
638 )
639 })
640 .unwrap()
641 }
642
643 pub fn build_from_fallback(self) -> Catalog {
650 self.build_inner(|_, _, _| Some(fallback_specs())).unwrap()
651 }
652
653 pub fn build_without_fallback(self) -> Option<Catalog> {
661 self.build_inner(|user_config_dir, system_config_dir, error_cb| {
662 read_specs(user_config_dir, system_config_dir, error_cb)
663 })
664 }
665
666 pub fn with_papersize_value(self, papersize: Option<&'a str>) -> Self {
670 Self {
671 papersize: Some(papersize),
672 ..self
673 }
674 }
675
676 pub fn without_locale(self) -> Self {
683 Self {
684 use_locale: false,
685 ..self
686 }
687 }
688
689 pub fn with_user_config_dir(self, user_config_dir: Option<&'a Path>) -> Self {
698 Self {
699 user_config_dir: Some(user_config_dir),
700 ..self
701 }
702 }
703
704 pub fn with_system_config_dir(self, system_config_dir: Option<&'a Path>) -> Self {
712 Self {
713 system_config_dir,
714 ..self
715 }
716 }
717
718 pub fn with_error_callback(self, error_cb: Box<dyn FnMut(CatalogBuildError) + 'a>) -> Self {
727 Self { error_cb, ..self }
728 }
729
730 fn build_inner<F>(mut self, f: F) -> Option<Catalog>
731 where
732 F: Fn(
733 Option<&Path>,
734 Option<&Path>,
735 &mut Box<dyn FnMut(CatalogBuildError) + 'a>,
736 ) -> Option<(Vec<PaperSpec>, PaperSpec)>,
737 {
738 let base_directories;
739 let user_config_dir = match self.user_config_dir {
740 Some(user_config_dir) => user_config_dir,
741 None => {
742 base_directories = BaseDirectories::new();
743 base_directories.config_home.as_deref()
744 }
745 };
746 let (specs, default) = f(user_config_dir, self.system_config_dir, &mut self.error_cb)?;
747 let default = match default_paper(
748 self.papersize,
749 user_config_dir,
750 self.use_locale,
751 self.system_config_dir,
752 &default,
753 &mut self.error_cb,
754 ) {
755 DefaultPaper::Name(name) => specs
756 .iter()
757 .find(|spec| spec.name.eq_ignore_ascii_case(&name))
758 .cloned()
759 .unwrap_or(default),
760 DefaultPaper::Size(size) => specs
761 .iter()
762 .find(|spec| spec.size.eq_rounded(&size, Unit::Point))
763 .cloned()
764 .unwrap_or_else(|| PaperSpec::new(Cow::from("Locale"), size)),
765 };
766
767 Some(Catalog { specs, default })
768 }
769}
770
771pub struct Catalog {
773 specs: Vec<PaperSpec>,
774 default: PaperSpec,
775}
776
777impl Default for Catalog {
778 fn default() -> Self {
779 Self::builder().build()
780 }
781}
782
783impl Catalog {
784 pub fn builder<'a>() -> CatalogBuilder<'a> {
786 CatalogBuilder::new()
787 }
788
789 pub fn new() -> Self {
794 Self::default()
795 }
796
797 pub fn specs(&self) -> &[PaperSpec] {
800 &self.specs
801 }
802
803 pub fn default_paper(&self) -> &PaperSpec {
809 &self.default
810 }
811
812 pub fn get_by_size(&self, size: &PaperSize) -> Option<&PaperSpec> {
815 self.specs
816 .iter()
817 .find(|spec| spec.size.eq_rounded(size, Unit::Point))
818 }
819
820 pub fn get_by_name(&self, name: &str) -> Option<&PaperSpec> {
823 self.specs
824 .iter()
825 .find(|spec| spec.name.eq_ignore_ascii_case(name))
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use std::{borrow::Cow, path::Path, str::FromStr};
832
833 use crate::{
834 A4, CatalogBuildError, CatalogBuilder, Length, PaperSize, PaperSpec, ParsePaperSizeError,
835 ParsePaperSpecError, Unit, locale,
836 };
837
838 #[test]
839 fn unit() {
840 assert_eq!(Unit::Point.to_string(), "pt");
841 assert_eq!(Unit::Millimeter.to_string(), "mm");
842 assert_eq!(Unit::Inch.to_string(), "in");
843
844 assert_eq!("pt".parse(), Ok(Unit::Point));
845 assert_eq!("mm".parse(), Ok(Unit::Millimeter));
846 assert_eq!("in".parse(), Ok(Unit::Inch));
847
848 assert_eq!(
849 format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Millimeter)),
850 "25.400"
851 );
852 assert_eq!(
853 format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Inch)),
854 "1.000"
855 );
856 assert_eq!(
857 format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Point)),
858 "72.000"
859 );
860 assert_eq!(
861 format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Millimeter)),
862 "12.700"
863 );
864 assert_eq!(
865 format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Inch)),
866 "0.500"
867 );
868 assert_eq!(
869 format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Point)),
870 "36.000"
871 );
872 assert_eq!(
873 format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Millimeter)),
874 "12.700"
875 );
876 assert_eq!(
877 format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Inch)),
878 "0.500"
879 );
880 assert_eq!(
881 format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Point)),
882 "36.000"
883 );
884 }
885
886 #[test]
887 fn length() {
888 assert_eq!(
889 format!(
890 "{:.3}",
891 Length::new(1.0, Unit::Inch).into_unit(Unit::Millimeter)
892 ),
893 "25.400"
894 );
895 assert_eq!(
896 format!("{:.3}", Length::new(1.0, Unit::Inch).into_unit(Unit::Inch)),
897 "1.000"
898 );
899 assert_eq!(
900 format!("{:.3}", Length::new(1.0, Unit::Inch).into_unit(Unit::Point)),
901 "72.000"
902 );
903 assert_eq!(
904 format!(
905 "{:.3}",
906 Length::new(36.0, Unit::Point).into_unit(Unit::Millimeter)
907 ),
908 "12.700"
909 );
910 assert_eq!(
911 format!(
912 "{:.3}",
913 Length::new(36.0, Unit::Point).into_unit(Unit::Inch)
914 ),
915 "0.500"
916 );
917 assert_eq!(
918 format!(
919 "{:.3}",
920 Length::new(36.0, Unit::Point).into_unit(Unit::Point)
921 ),
922 "36.000"
923 );
924 assert_eq!(
925 format!(
926 "{:.3}",
927 Length::new(12.7, Unit::Millimeter).into_unit(Unit::Millimeter)
928 ),
929 "12.700"
930 );
931 assert_eq!(
932 format!(
933 "{:.3}",
934 Length::new(12.7, Unit::Millimeter).into_unit(Unit::Inch)
935 ),
936 "0.500"
937 );
938 assert_eq!(
939 format!(
940 "{:.3}",
941 Length::new(12.7, Unit::Millimeter).into_unit(Unit::Point)
942 ),
943 "36.000"
944 );
945 }
946
947 #[test]
948 fn papersize() {
949 assert_eq!(
950 "8.5x11in".parse(),
951 Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
952 );
953 assert_eq!(
954 "8.5,11in".parse(),
955 Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
956 );
957 assert_eq!(
958 " 8.5 x 11 in ".parse(),
959 Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
960 );
961 assert_eq!(
962 PaperSize::from_str("8.5x.in"),
963 Err(ParsePaperSizeError::InvalidHeight)
964 );
965 assert_eq!(
966 PaperSize::from_str(".x11in"),
967 Err(ParsePaperSizeError::InvalidWidth)
968 );
969 assert_eq!(
970 PaperSize::from_str("8.5x11xyzzy"),
971 Err(ParsePaperSizeError::InvalidUnit)
972 );
973 assert_eq!(
974 PaperSize::from_str("8.5x11"),
975 Err(ParsePaperSizeError::MissingUnit)
976 );
977 assert_eq!(
978 PaperSize::from_str(" 8.5 11 in "),
979 Err(ParsePaperSizeError::MissingDelimiter)
980 );
981 assert_eq!(
982 PaperSize::new(8.5, 11.0, Unit::Inch).to_string(),
983 "8.5x11in"
984 );
985 assert_eq!(A4.size.to_string(), "210x297mm");
986 }
987
988 #[test]
989 fn paperspec() {
990 assert_eq!(
991 "Letter,8.5,11,in".parse(),
992 Ok(PaperSpec::new(
993 Cow::from("Letter"),
994 PaperSize::new(8.5, 11.0, Unit::Inch)
995 ))
996 );
997 assert_eq!(
998 "Letter,8.5x11in".parse(),
999 Ok(PaperSpec::new(
1000 Cow::from("Letter"),
1001 PaperSize::new(8.5, 11.0, Unit::Inch)
1002 ))
1003 );
1004 }
1005
1006 #[test]
1007 fn default() {
1008 assert_eq!(
1010 CatalogBuilder::new()
1011 .with_papersize_value(Some("legal"))
1012 .with_user_config_dir(Some(Path::new("testdata/td1")))
1013 .without_locale()
1014 .build_from_fallback()
1015 .default_paper(),
1016 &PaperSpec::new(Cow::from("Legal"), PaperSize::new(8.5, 14.0, Unit::Inch))
1017 );
1018
1019 assert_eq!(
1021 CatalogBuilder::new()
1022 .with_papersize_value(None)
1023 .with_user_config_dir(Some(Path::new("testdata/td1")))
1024 .without_locale()
1025 .build_from_fallback()
1026 .default_paper(),
1027 &PaperSpec::new(Cow::from("Ledger"), PaperSize::new(17.0, 11.0, Unit::Inch))
1028 );
1029
1030 assert_eq!(
1032 CatalogBuilder::new()
1033 .with_papersize_value(None)
1034 .with_user_config_dir(None)
1035 .with_system_config_dir(Some(Path::new("testdata/td2")))
1036 .without_locale()
1037 .build_from_fallback()
1038 .default_paper(),
1039 &PaperSpec::new(
1040 Cow::from("Executive"),
1041 PaperSize::new(7.25, 10.5, Unit::Inch)
1042 )
1043 );
1044
1045 assert_eq!(
1047 CatalogBuilder::new()
1048 .with_papersize_value(None)
1049 .with_user_config_dir(None)
1050 .with_system_config_dir(Some(Path::new("testdata/td2")))
1051 .without_locale()
1052 .build()
1053 .default_paper(),
1054 &PaperSpec::new(
1055 Cow::from("A0"),
1056 PaperSize::new(841.0, 1189.0, Unit::Millimeter)
1057 )
1058 );
1059
1060 assert_eq!(
1062 CatalogBuilder::new()
1063 .with_papersize_value(None)
1064 .with_user_config_dir(Some(Path::new("testdata/td3")))
1065 .with_system_config_dir(None)
1066 .without_locale()
1067 .build()
1068 .default_paper(),
1069 &PaperSpec::new(
1070 Cow::from("B0"),
1071 PaperSize::new(1000.0, 1414.0, Unit::Millimeter)
1072 )
1073 );
1074
1075 assert_eq!(
1077 CatalogBuilder::new()
1078 .with_papersize_value(None)
1079 .with_user_config_dir(None)
1080 .with_system_config_dir(None)
1081 .without_locale()
1082 .build()
1083 .default_paper(),
1084 &PaperSpec::new(
1085 Cow::from("A4"),
1086 PaperSize::new(210.0, 297.0, Unit::Millimeter)
1087 )
1088 );
1089
1090 assert!(
1092 CatalogBuilder::new()
1093 .with_papersize_value(None)
1094 .with_user_config_dir(None)
1095 .with_system_config_dir(None)
1096 .without_locale()
1097 .build_without_fallback()
1098 .is_none()
1099 );
1100 }
1101
1102 #[test]
1103 fn errors() {
1104 let mut errors = Vec::new();
1106 let _ = CatalogBuilder::new()
1107 .with_papersize_value(None)
1108 .with_user_config_dir(Some(Path::new("nonexistent/user")))
1109 .with_system_config_dir(Some(Path::new("nonexistent/system")))
1110 .without_locale()
1111 .with_error_callback(Box::new(|error| errors.push(error)))
1112 .build()
1113 .default_paper();
1114 assert_eq!(errors.len(), 0);
1115
1116 let mut errors = Vec::new();
1118 let _ = CatalogBuilder::new()
1119 .with_papersize_value(None)
1120 .with_user_config_dir(None)
1121 .with_system_config_dir(Some(Path::new("testdata/td4")))
1122 .without_locale()
1123 .with_error_callback(Box::new(|error| errors.push(error)))
1124 .build()
1125 .default_paper();
1126
1127 assert_eq!(errors.len(), 4);
1128 for ((error, expect_line_number), expect_error) in errors.iter().zip(1..).zip([
1129 ParsePaperSpecError::MissingField,
1130 ParsePaperSpecError::InvalidWidth,
1131 ParsePaperSpecError::InvalidHeight,
1132 ParsePaperSpecError::InvalidUnit,
1133 ]) {
1134 let CatalogBuildError::ParseError {
1135 path,
1136 line_number,
1137 error,
1138 } = error
1139 else {
1140 unreachable!()
1141 };
1142 assert_eq!(path.as_path(), Path::new("testdata/td4/paperspecs"));
1143 assert_eq!(*line_number, expect_line_number);
1144 assert_eq!(*error, expect_error);
1145 }
1146 }
1147
1148 #[cfg(target_os = "linux")]
1149 #[test]
1150 fn lc_paper() {
1151 if let Some(size) = locale::locale_paper_size() {
1156 assert_eq!(size.unit, Unit::Millimeter);
1157 let (w, h) = size.into_width_height();
1158 assert!(
1159 (w, h) == (210.0, 297.0) || (w, h) == (216.0, 279.0),
1160 "Expected A4 (210x297) or letter (216x279) paper, got {w}x{h} mm"
1161 );
1162 }
1163 }
1164}