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
45include!(concat!(env!("OUT_DIR"), "/paperspecs.rs"));
46
47static PAPERSIZE_FILENAME: &str = "papersize";
48static PAPERSPECS_FILENAME: &str = "paperspecs";
49
50enum DefaultPaper {
51 Name(String),
52 Size(PaperSize),
53}
54
55#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57pub enum Unit {
58 Point,
60
61 Inch,
63
64 Millimeter,
66}
67
68#[derive(Copy, Clone, Debug, PartialEq, Eq)]
70pub struct ParseUnitError;
71
72impl FromStr for Unit {
73 type Err = ParseUnitError;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 match s {
79 "pt" => Ok(Self::Point),
80 "in" => Ok(Self::Inch),
81 "mm" => Ok(Self::Millimeter),
82 _ => Err(ParseUnitError),
83 }
84 }
85}
86
87impl Unit {
88 pub fn name(&self) -> &'static str {
90 match self {
91 Unit::Point => "pt",
92 Unit::Inch => "in",
93 Unit::Millimeter => "mm",
94 }
95 }
96
97 fn as_unit(&self, other: Unit) -> f64 {
102 match (*self, other) {
103 (Unit::Point, Unit::Point) => 1.0,
104 (Unit::Point, Unit::Inch) => 1.0 / 72.0,
105 (Unit::Point, Unit::Millimeter) => 25.4 / 72.0,
106 (Unit::Inch, Unit::Point) => 72.0,
107 (Unit::Inch, Unit::Inch) => 1.0,
108 (Unit::Inch, Unit::Millimeter) => 25.4,
109 (Unit::Millimeter, Unit::Point) => 72.0 / 25.4,
110 (Unit::Millimeter, Unit::Inch) => 1.0 / 25.4,
111 (Unit::Millimeter, Unit::Millimeter) => 1.0,
112 }
113 }
114}
115
116impl Display for Unit {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(f, "{}", self.name())
119 }
120}
121
122#[derive(Copy, Clone, Debug, PartialEq)]
124pub struct PaperSize {
125 pub width: f64,
127
128 pub height: f64,
130
131 pub unit: Unit,
133}
134
135impl Default for PaperSize {
136 fn default() -> Self {
138 Self::new(210.0, 297.0, Unit::Millimeter)
139 }
140}
141
142impl PaperSize {
143 pub fn new(width: f64, height: f64, unit: Unit) -> Self {
145 Self {
146 width,
147 height,
148 unit,
149 }
150 }
151
152 pub fn as_unit(&self, unit: Unit) -> PaperSize {
154 Self {
155 width: self.width * self.unit.as_unit(unit),
156 height: self.height * self.unit.as_unit(unit),
157 unit,
158 }
159 }
160
161 pub fn into_width_height(self) -> (f64, f64) {
163 (self.width, self.height)
164 }
165
166 pub fn eq_rounded(&self, other: &Self, unit: Unit) -> bool {
169 let (aw, ah) = self.as_unit(unit).into_width_height();
170 let (bw, bh) = other.as_unit(unit).into_width_height();
171 aw.round() == bw.round() && ah.round() == bh.round()
172 }
173}
174
175#[derive(Copy, Clone, Debug, PartialEq, Eq)]
177pub enum ParsePaperSizeError {
178 InvalidHeight,
180
181 InvalidWidth,
183
184 InvalidUnit,
186
187 MissingUnit,
189
190 MissingDelimiter,
192}
193
194impl Error for ParsePaperSizeError {}
195
196impl Display for ParsePaperSizeError {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 match self {
199 ParsePaperSizeError::InvalidHeight => write!(f, "Invalid paper height"),
200 ParsePaperSizeError::InvalidWidth => write!(f, "Invalid paper width"),
201 ParsePaperSizeError::InvalidUnit => write!(f, "Invalid unit of measurement"),
202 ParsePaperSizeError::MissingUnit => write!(f, "Missing unit in paper size"),
203 ParsePaperSizeError::MissingDelimiter => write!(f, "Missing delimiter in paper size"),
204 }
205 }
206}
207
208impl FromStr for PaperSize {
209 type Err = ParsePaperSizeError;
210
211 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 let Some((width, rest)) = s.split_once([',', 'x']) else {
215 return Err(ParsePaperSizeError::MissingDelimiter);
216 };
217 let (height, unit) = if let Some(result) = rest.split_once(',') {
218 result
219 } else if let Some(alpha) = rest.find(|c: char| c.is_alphabetic()) {
220 rest.split_at(alpha)
221 } else {
222 return Err(ParsePaperSizeError::MissingUnit);
223 };
224
225 let width = f64::from_str(width.trim()).map_err(|_| ParsePaperSizeError::InvalidWidth)?;
226 let height =
227 f64::from_str(height.trim()).map_err(|_| ParsePaperSizeError::InvalidHeight)?;
228 let unit = Unit::from_str(unit.trim()).map_err(|_| ParsePaperSizeError::InvalidUnit)?;
229 Ok(Self {
230 width,
231 height,
232 unit,
233 })
234 }
235}
236
237impl Display for PaperSize {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 write!(f, "{}x{}{}", self.width, self.height, self.unit)
240 }
241}
242
243#[cfg(feature = "serde")]
244impl serde::Serialize for PaperSize {
245 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
246 where
247 S: serde::Serializer,
248 {
249 self.to_string().serialize(serializer)
250 }
251}
252
253#[cfg(feature = "serde")]
254impl<'de> serde::Deserialize<'de> for PaperSize {
255 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
256 where
257 D: serde::Deserializer<'de>,
258 {
259 use serde::de::Error;
260 String::deserialize(deserializer)?
261 .parse()
262 .map_err(D::Error::custom)
263 }
264}
265
266#[derive(Copy, Clone, Debug, PartialEq, Eq)]
268pub enum ParsePaperSpecError {
269 InvalidHeight,
271
272 InvalidWidth,
274
275 InvalidUnit,
277
278 MissingField,
280}
281
282impl From<ParsePaperSizeError> for ParsePaperSpecError {
283 fn from(value: ParsePaperSizeError) -> Self {
284 match value {
285 ParsePaperSizeError::InvalidHeight => Self::InvalidHeight,
286 ParsePaperSizeError::InvalidWidth => Self::InvalidWidth,
287 ParsePaperSizeError::InvalidUnit => Self::InvalidUnit,
288 ParsePaperSizeError::MissingUnit => Self::MissingField,
289 ParsePaperSizeError::MissingDelimiter => Self::MissingField,
290 }
291 }
292}
293
294impl Error for ParsePaperSpecError {}
295
296impl Display for ParsePaperSpecError {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 ParsePaperSpecError::InvalidHeight => write!(f, "Invalid paper height."),
300 ParsePaperSpecError::InvalidWidth => write!(f, "Invalid paper width."),
301 ParsePaperSpecError::InvalidUnit => write!(f, "Invalid unit of measurement."),
302 ParsePaperSpecError::MissingField => write!(f, "Missing field in paper specification."),
303 }
304 }
305}
306
307#[derive(Clone, Debug, PartialEq)]
309pub struct PaperSpec {
310 pub name: Cow<'static, str>,
312
313 pub size: PaperSize,
315}
316
317impl PaperSpec {
318 pub fn new(name: impl Into<Cow<'static, str>>, size: PaperSize) -> Self {
320 Self {
321 name: name.into(),
322 size,
323 }
324 }
325}
326
327impl FromStr for PaperSpec {
328 type Err = ParsePaperSpecError;
329
330 fn from_str(s: &str) -> Result<Self, Self::Err> {
335 let (name, size) = s.split_once(',').ok_or(ParsePaperSpecError::MissingField)?;
336 Ok(Self {
337 name: String::from(name).into(),
338 size: size.parse()?,
339 })
340 }
341}
342
343#[derive(Debug)]
345pub enum CatalogBuildError {
346 ParseError {
348 path: PathBuf,
350
351 line_number: usize,
353
354 error: ParsePaperSpecError,
356 },
357
358 IoError {
360 path: PathBuf,
362
363 error: std::io::Error,
365 },
366}
367
368impl Error for CatalogBuildError {}
369
370impl Display for CatalogBuildError {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 match self {
373 CatalogBuildError::ParseError {
374 path,
375 line_number,
376 error,
377 } => write!(f, "{}:{line_number}: {error}", path.display()),
378 CatalogBuildError::IoError { path, error } => {
379 write!(f, "{}: {error}", path.display())
380 }
381 }
382 }
383}
384
385pub struct CatalogBuilder<'a> {
391 papersize: Option<Option<&'a str>>,
392 use_locale: bool,
393 user_config_dir: Option<Option<&'a Path>>,
394 system_config_dir: Option<&'a Path>,
395 error_cb: Box<dyn FnMut(CatalogBuildError) + 'a>,
396}
397
398impl<'a> Default for CatalogBuilder<'a> {
399 fn default() -> Self {
400 Self {
401 use_locale: true,
402 papersize: None,
403 user_config_dir: None,
404 system_config_dir: Some(Path::new("/etc")),
405 error_cb: Box::new(drop),
406 }
407 }
408}
409
410fn fallback_specs() -> (Vec<PaperSpec>, PaperSpec) {
411 let specs = STANDARD_PAPERSPECS.into_iter().cloned().collect::<Vec<_>>();
412 let default = specs.first().unwrap().clone();
413 (specs, default)
414}
415
416fn read_specs<E>(
417 user_config_dir: Option<&Path>,
418 system_config_dir: Option<&Path>,
419 mut error_cb: E,
420) -> Option<(Vec<PaperSpec>, PaperSpec)>
421where
422 E: FnMut(CatalogBuildError),
423{
424 fn read_paperspecs_file(
425 directory: Option<&Path>,
426 error_cb: &mut dyn FnMut(CatalogBuildError),
427 ) -> Vec<PaperSpec> {
428 let mut specs = Vec::new();
429 if let Some(directory) = directory {
430 let path = directory.join(PAPERSPECS_FILENAME);
431 match File::open(&path) {
432 Ok(file) => {
433 let reader = BufReader::new(file);
434 for (line, line_number) in reader.lines().zip(1..) {
435 match line
436 .map_err(|error| CatalogBuildError::IoError {
437 path: path.clone(),
438 error,
439 })
440 .and_then(|line| {
441 PaperSpec::from_str(&line).map_err(|error| {
442 CatalogBuildError::ParseError {
443 path: path.clone(),
444 line_number,
445 error,
446 }
447 })
448 }) {
449 Ok(spec) => specs.push(spec),
450 Err(error) => error_cb(error),
451 }
452 }
453 }
454 Err(error) if error.kind() == ErrorKind::NotFound => (),
455 Err(error) => error_cb(CatalogBuildError::IoError { path, error }),
456 }
457 }
458 specs
459 }
460
461 let user_specs = read_paperspecs_file(user_config_dir, &mut error_cb);
462 let system_specs = read_paperspecs_file(system_config_dir, &mut error_cb);
463 let default_spec = system_specs.first().or(user_specs.first())?.clone();
464 Some((
465 user_specs.into_iter().chain(system_specs).collect(),
466 default_spec,
467 ))
468}
469
470fn default_paper<E>(
471 papersize: Option<Option<&str>>,
472 user_config_dir: Option<&Path>,
473 _use_locale: bool,
474 system_config_dir: Option<&Path>,
475 default: &PaperSpec,
476 mut error_cb: E,
477) -> DefaultPaper
478where
479 E: FnMut(CatalogBuildError),
480{
481 fn read_papersize_file<P, E>(path: P, mut error_cb: E) -> Option<String>
482 where
483 P: AsRef<Path>,
484 E: FnMut(CatalogBuildError),
485 {
486 fn inner(path: &Path) -> std::io::Result<Option<String>> {
487 let file = BufReader::new(File::open(path)?);
488 let line = file.lines().next().unwrap_or(Ok(String::new()))?;
489 let name = line.split(',').next().unwrap_or("");
490 Ok(name.is_empty().not().then(|| name.into()))
491 }
492 let path = path.as_ref();
493 match inner(path) {
494 Ok(result) => result,
495 Err(error) => {
496 if error.kind() != ErrorKind::NotFound {
497 error_cb(CatalogBuildError::IoError {
498 path: path.to_path_buf(),
499 error,
500 });
501 }
502 None
503 }
504 }
505 }
506
507 let env_var;
509 let paper_name = match papersize {
510 Some(paper_name) => paper_name,
511 None => {
512 env_var = std::env::var("PAPERSIZE").ok();
513 env_var.as_deref()
514 }
515 };
516 if let Some(paper_name) = paper_name
517 && !paper_name.is_empty()
518 {
519 return DefaultPaper::Name(paper_name.into());
520 }
521
522 if let Some(dir) = user_config_dir
524 && let path = dir.join(PAPERSIZE_FILENAME)
525 && let Some(paper_name) = read_papersize_file(path, &mut error_cb)
526 {
527 return DefaultPaper::Name(paper_name);
528 }
529
530 #[cfg(target_os = "linux")]
532 if _use_locale && let Some(paper_size) = locale::locale_paper_size() {
533 return DefaultPaper::Size(paper_size);
534 }
535
536 if let Some(system_config_dir) = system_config_dir
537 && let Some(paper_name) =
538 read_papersize_file(system_config_dir.join(PAPERSIZE_FILENAME), &mut error_cb)
539 {
540 return DefaultPaper::Name(paper_name);
541 }
542
543 DefaultPaper::Name(default.name.as_ref().into())
545}
546
547impl<'a> CatalogBuilder<'a> {
548 pub fn new() -> Self {
550 Self::default()
551 }
552
553 pub fn build(self) -> Catalog {
562 self.build_inner(|user_config_dir, system_config_dir, error_cb| {
563 Some(
564 read_specs(user_config_dir, system_config_dir, error_cb)
565 .unwrap_or_else(fallback_specs),
566 )
567 })
568 .unwrap()
569 }
570
571 pub fn build_from_fallback(self) -> Catalog {
578 self.build_inner(|_, _, _| Some(fallback_specs())).unwrap()
579 }
580
581 pub fn build_without_fallback(self) -> Option<Catalog> {
589 self.build_inner(|user_config_dir, system_config_dir, error_cb| {
590 read_specs(user_config_dir, system_config_dir, error_cb)
591 })
592 }
593
594 pub fn with_papersize_value(self, papersize: Option<&'a str>) -> Self {
598 Self {
599 papersize: Some(papersize),
600 ..self
601 }
602 }
603
604 pub fn without_locale(self) -> Self {
611 Self {
612 use_locale: false,
613 ..self
614 }
615 }
616
617 pub fn with_user_config_dir(self, user_config_dir: Option<&'a Path>) -> Self {
626 Self {
627 user_config_dir: Some(user_config_dir),
628 ..self
629 }
630 }
631
632 pub fn with_system_config_dir(self, system_config_dir: Option<&'a Path>) -> Self {
640 Self {
641 system_config_dir,
642 ..self
643 }
644 }
645
646 pub fn with_error_callback(self, error_cb: Box<dyn FnMut(CatalogBuildError) + 'a>) -> Self {
655 Self { error_cb, ..self }
656 }
657
658 fn build_inner<F>(mut self, f: F) -> Option<Catalog>
659 where
660 F: Fn(
661 Option<&Path>,
662 Option<&Path>,
663 &mut Box<dyn FnMut(CatalogBuildError) + 'a>,
664 ) -> Option<(Vec<PaperSpec>, PaperSpec)>,
665 {
666 let base_directories;
667 let user_config_dir = match self.user_config_dir {
668 Some(user_config_dir) => user_config_dir,
669 None => {
670 base_directories = BaseDirectories::new();
671 base_directories.config_home.as_deref()
672 }
673 };
674 let (specs, default) = f(user_config_dir, self.system_config_dir, &mut self.error_cb)?;
675 let default = match default_paper(
676 self.papersize,
677 user_config_dir,
678 self.use_locale,
679 self.system_config_dir,
680 &default,
681 &mut self.error_cb,
682 ) {
683 DefaultPaper::Name(name) => specs
684 .iter()
685 .find(|spec| spec.name.eq_ignore_ascii_case(&name))
686 .cloned()
687 .unwrap_or(default),
688 DefaultPaper::Size(size) => specs
689 .iter()
690 .find(|spec| spec.size.eq_rounded(&size, Unit::Point))
691 .cloned()
692 .unwrap_or_else(|| PaperSpec::new(Cow::from("Locale"), size)),
693 };
694
695 Some(Catalog { specs, default })
696 }
697}
698
699pub struct Catalog {
701 specs: Vec<PaperSpec>,
702 default: PaperSpec,
703}
704
705impl Default for Catalog {
706 fn default() -> Self {
707 Self::builder().build()
708 }
709}
710
711impl Catalog {
712 pub fn builder<'a>() -> CatalogBuilder<'a> {
714 CatalogBuilder::new()
715 }
716
717 pub fn new() -> Self {
722 Self::default()
723 }
724
725 pub fn specs(&self) -> &[PaperSpec] {
728 &self.specs
729 }
730
731 pub fn default_paper(&self) -> &PaperSpec {
737 &self.default
738 }
739
740 pub fn get_by_size(&self, size: &PaperSize) -> Option<&PaperSpec> {
743 self.specs
744 .iter()
745 .find(|spec| spec.size.eq_rounded(size, Unit::Point))
746 }
747
748 pub fn get_by_name(&self, name: &str) -> Option<&PaperSpec> {
751 self.specs
752 .iter()
753 .find(|spec| spec.name.eq_ignore_ascii_case(name))
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use std::{borrow::Cow, path::Path, str::FromStr};
760
761 use crate::{
762 A4, CatalogBuildError, CatalogBuilder, PaperSize, PaperSpec, ParsePaperSizeError,
763 ParsePaperSpecError, Unit, locale,
764 };
765
766 #[test]
767 fn unit() {
768 assert_eq!(Unit::Point.to_string(), "pt");
769 assert_eq!(Unit::Millimeter.to_string(), "mm");
770 assert_eq!(Unit::Inch.to_string(), "in");
771
772 assert_eq!("pt".parse(), Ok(Unit::Point));
773 assert_eq!("mm".parse(), Ok(Unit::Millimeter));
774 assert_eq!("in".parse(), Ok(Unit::Inch));
775
776 assert_eq!(
777 format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Millimeter)),
778 "25.400"
779 );
780 assert_eq!(
781 format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Inch)),
782 "1.000"
783 );
784 assert_eq!(
785 format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Point)),
786 "72.000"
787 );
788 assert_eq!(
789 format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Millimeter)),
790 "12.700"
791 );
792 assert_eq!(
793 format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Inch)),
794 "0.500"
795 );
796 assert_eq!(
797 format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Point)),
798 "36.000"
799 );
800 assert_eq!(
801 format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Millimeter)),
802 "12.700"
803 );
804 assert_eq!(
805 format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Inch)),
806 "0.500"
807 );
808 assert_eq!(
809 format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Point)),
810 "36.000"
811 );
812 }
813
814 #[test]
815 fn papersize() {
816 assert_eq!(
817 "8.5x11in".parse(),
818 Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
819 );
820 assert_eq!(
821 "8.5,11in".parse(),
822 Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
823 );
824 assert_eq!(
825 " 8.5 x 11 in ".parse(),
826 Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
827 );
828 assert_eq!(
829 PaperSize::from_str("8.5x.in"),
830 Err(ParsePaperSizeError::InvalidHeight)
831 );
832 assert_eq!(
833 PaperSize::from_str(".x11in"),
834 Err(ParsePaperSizeError::InvalidWidth)
835 );
836 assert_eq!(
837 PaperSize::from_str("8.5x11xyzzy"),
838 Err(ParsePaperSizeError::InvalidUnit)
839 );
840 assert_eq!(
841 PaperSize::from_str("8.5x11"),
842 Err(ParsePaperSizeError::MissingUnit)
843 );
844 assert_eq!(
845 PaperSize::from_str(" 8.5 11 in "),
846 Err(ParsePaperSizeError::MissingDelimiter)
847 );
848 assert_eq!(
849 PaperSize::new(8.5, 11.0, Unit::Inch).to_string(),
850 "8.5x11in"
851 );
852 assert_eq!(A4.size.to_string(), "210x297mm");
853 }
854
855 #[test]
856 fn paperspec() {
857 assert_eq!(
858 "Letter,8.5,11,in".parse(),
859 Ok(PaperSpec::new(
860 Cow::from("Letter"),
861 PaperSize::new(8.5, 11.0, Unit::Inch)
862 ))
863 );
864 assert_eq!(
865 "Letter,8.5x11in".parse(),
866 Ok(PaperSpec::new(
867 Cow::from("Letter"),
868 PaperSize::new(8.5, 11.0, Unit::Inch)
869 ))
870 );
871 }
872
873 #[test]
874 fn default() {
875 assert_eq!(
877 CatalogBuilder::new()
878 .with_papersize_value(Some("legal"))
879 .with_user_config_dir(Some(Path::new("testdata/td1")))
880 .without_locale()
881 .build_from_fallback()
882 .default_paper(),
883 &PaperSpec::new(Cow::from("Legal"), PaperSize::new(8.5, 14.0, Unit::Inch))
884 );
885
886 assert_eq!(
888 CatalogBuilder::new()
889 .with_papersize_value(None)
890 .with_user_config_dir(Some(Path::new("testdata/td1")))
891 .without_locale()
892 .build_from_fallback()
893 .default_paper(),
894 &PaperSpec::new(Cow::from("Ledger"), PaperSize::new(17.0, 11.0, Unit::Inch))
895 );
896
897 assert_eq!(
899 CatalogBuilder::new()
900 .with_papersize_value(None)
901 .with_user_config_dir(None)
902 .with_system_config_dir(Some(Path::new("testdata/td2")))
903 .without_locale()
904 .build_from_fallback()
905 .default_paper(),
906 &PaperSpec::new(
907 Cow::from("Executive"),
908 PaperSize::new(7.25, 10.5, Unit::Inch)
909 )
910 );
911
912 assert_eq!(
914 CatalogBuilder::new()
915 .with_papersize_value(None)
916 .with_user_config_dir(None)
917 .with_system_config_dir(Some(Path::new("testdata/td2")))
918 .without_locale()
919 .build()
920 .default_paper(),
921 &PaperSpec::new(
922 Cow::from("A0"),
923 PaperSize::new(841.0, 1189.0, Unit::Millimeter)
924 )
925 );
926
927 assert_eq!(
929 CatalogBuilder::new()
930 .with_papersize_value(None)
931 .with_user_config_dir(Some(Path::new("testdata/td3")))
932 .with_system_config_dir(None)
933 .without_locale()
934 .build()
935 .default_paper(),
936 &PaperSpec::new(
937 Cow::from("B0"),
938 PaperSize::new(1000.0, 1414.0, Unit::Millimeter)
939 )
940 );
941
942 assert_eq!(
944 CatalogBuilder::new()
945 .with_papersize_value(None)
946 .with_user_config_dir(None)
947 .with_system_config_dir(None)
948 .without_locale()
949 .build()
950 .default_paper(),
951 &PaperSpec::new(
952 Cow::from("A4"),
953 PaperSize::new(210.0, 297.0, Unit::Millimeter)
954 )
955 );
956
957 assert!(
959 CatalogBuilder::new()
960 .with_papersize_value(None)
961 .with_user_config_dir(None)
962 .with_system_config_dir(None)
963 .without_locale()
964 .build_without_fallback()
965 .is_none()
966 );
967 }
968
969 #[test]
970 fn errors() {
971 let mut errors = Vec::new();
973 let _ = CatalogBuilder::new()
974 .with_papersize_value(None)
975 .with_user_config_dir(Some(Path::new("nonexistent/user")))
976 .with_system_config_dir(Some(Path::new("nonexistent/system")))
977 .without_locale()
978 .with_error_callback(Box::new(|error| errors.push(error)))
979 .build()
980 .default_paper();
981 assert_eq!(errors.len(), 0);
982
983 let mut errors = Vec::new();
985 let _ = CatalogBuilder::new()
986 .with_papersize_value(None)
987 .with_user_config_dir(None)
988 .with_system_config_dir(Some(Path::new("testdata/td4")))
989 .without_locale()
990 .with_error_callback(Box::new(|error| errors.push(error)))
991 .build()
992 .default_paper();
993
994 assert_eq!(errors.len(), 4);
995 for ((error, expect_line_number), expect_error) in errors.iter().zip(1..).zip([
996 ParsePaperSpecError::MissingField,
997 ParsePaperSpecError::InvalidWidth,
998 ParsePaperSpecError::InvalidHeight,
999 ParsePaperSpecError::InvalidUnit,
1000 ]) {
1001 let CatalogBuildError::ParseError {
1002 path,
1003 line_number,
1004 error,
1005 } = error
1006 else {
1007 unreachable!()
1008 };
1009 assert_eq!(path.as_path(), Path::new("testdata/td4/paperspecs"));
1010 assert_eq!(*line_number, expect_line_number);
1011 assert_eq!(*error, expect_error);
1012 }
1013 }
1014
1015 #[cfg(target_os = "linux")]
1016 #[test]
1017 fn lc_paper() {
1018 if let Some(size) = locale::locale_paper_size() {
1023 assert_eq!(size.unit, Unit::Millimeter);
1024 let (w, h) = size.into_width_height();
1025 assert!(
1026 (w, h) == (210.0, 297.0) || (w, h) == (216.0, 279.0),
1027 "Expected A4 (210x297) or letter (216x279) paper, got {w}x{h} mm"
1028 );
1029 }
1030 }
1031
1032 #[cfg(feature = "serde")]
1033 #[test]
1034 fn test_serde() {
1035 assert_eq!(
1036 serde_json::to_string(&PaperSize::new(8.5, 11.0, Unit::Inch)).unwrap(),
1037 "\"8.5x11in\""
1038 );
1039 assert_eq!(
1040 serde_json::from_str::<PaperSize>("\"8.5x11in\"").unwrap(),
1041 PaperSize::new(8.5, 11.0, Unit::Inch)
1042 )
1043 }
1044}