#![warn(missing_docs)]
use std::{
borrow::Cow,
error::Error,
fmt::Display,
fs::File,
io::{BufRead, BufReader, ErrorKind},
ops::Not,
path::{Path, PathBuf},
str::FromStr,
};
use xdg::BaseDirectories;
#[cfg(target_os = "linux")]
mod locale;
#[cfg(feature = "serde")]
mod serde;
include!(concat!(env!("OUT_DIR"), "/paperspecs.rs"));
static PAPERSIZE_FILENAME: &str = "papersize";
static PAPERSPECS_FILENAME: &str = "paperspecs";
enum DefaultPaper {
Name(String),
Size(PaperSize),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Unit {
Point,
Inch,
Millimeter,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ParseUnitError;
impl Error for ParseUnitError {}
impl Display for ParseUnitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "unknown unit")
}
}
impl FromStr for Unit {
type Err = ParseUnitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"pt" => Ok(Self::Point),
"in" => Ok(Self::Inch),
"mm" => Ok(Self::Millimeter),
_ => Err(ParseUnitError),
}
}
}
impl Unit {
pub fn name(&self) -> &'static str {
match self {
Unit::Point => "pt",
Unit::Inch => "in",
Unit::Millimeter => "mm",
}
}
pub fn as_unit(&self, other: Unit) -> f64 {
match (*self, other) {
(Unit::Point, Unit::Point) => 1.0,
(Unit::Point, Unit::Inch) => 1.0 / 72.0,
(Unit::Point, Unit::Millimeter) => 25.4 / 72.0,
(Unit::Inch, Unit::Point) => 72.0,
(Unit::Inch, Unit::Inch) => 1.0,
(Unit::Inch, Unit::Millimeter) => 25.4,
(Unit::Millimeter, Unit::Point) => 72.0 / 25.4,
(Unit::Millimeter, Unit::Inch) => 1.0 / 25.4,
(Unit::Millimeter, Unit::Millimeter) => 1.0,
}
}
}
impl Display for Unit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Length {
pub value: f64,
pub unit: Unit,
}
impl Length {
pub fn new(value: f64, unit: Unit) -> Self {
Self { value, unit }
}
pub fn as_unit(&self, unit: Unit) -> Self {
Self {
value: self.value * self.unit.as_unit(unit),
unit,
}
}
pub fn into_unit(&self, unit: Unit) -> f64 {
self.as_unit(unit).value
}
}
#[derive(Copy, Clone, Debug)]
pub enum ParseLengthError {
MissingUnit,
InvalidUnit,
InvalidValue,
}
impl Error for ParseLengthError {}
impl Display for ParseLengthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseLengthError::MissingUnit => write!(f, "Missing unit"),
ParseLengthError::InvalidUnit => write!(f, "Invalid unit of measurement"),
ParseLengthError::InvalidValue => write!(f, "Invalid length"),
}
}
}
impl FromStr for Length {
type Err = ParseLengthError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(index) = s.find(|c: char| c.is_alphabetic()) {
let (value, unit) = s.split_at(index);
let value = value.parse().map_err(|_| ParseLengthError::InvalidValue)?;
let unit = unit.parse().map_err(|_| ParseLengthError::InvalidUnit)?;
Ok(Self { value, unit })
} else {
Err(ParseLengthError::MissingUnit)
}
}
}
impl Display for Length {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", self.value, self.unit)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PaperSize {
pub width: f64,
pub height: f64,
pub unit: Unit,
}
impl Default for PaperSize {
fn default() -> Self {
Self::new(210.0, 297.0, Unit::Millimeter)
}
}
impl PaperSize {
pub fn new(width: f64, height: f64, unit: Unit) -> Self {
Self {
width,
height,
unit,
}
}
pub fn as_unit(&self, unit: Unit) -> PaperSize {
Self {
width: self.width * self.unit.as_unit(unit),
height: self.height * self.unit.as_unit(unit),
unit,
}
}
pub fn into_width_height(self) -> (f64, f64) {
(self.width, self.height)
}
pub fn eq_rounded(&self, other: &Self, unit: Unit) -> bool {
let (aw, ah) = self.as_unit(unit).into_width_height();
let (bw, bh) = other.as_unit(unit).into_width_height();
aw.round() == bw.round() && ah.round() == bh.round()
}
pub fn width(&self) -> Length {
Length::new(self.width, self.unit)
}
pub fn height(&self) -> Length {
Length::new(self.height, self.unit)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ParsePaperSizeError {
InvalidHeight,
InvalidWidth,
InvalidUnit,
MissingUnit,
MissingDelimiter,
}
impl Error for ParsePaperSizeError {}
impl Display for ParsePaperSizeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParsePaperSizeError::InvalidHeight => write!(f, "Invalid paper height"),
ParsePaperSizeError::InvalidWidth => write!(f, "Invalid paper width"),
ParsePaperSizeError::InvalidUnit => write!(f, "Invalid unit of measurement"),
ParsePaperSizeError::MissingUnit => write!(f, "Missing unit in paper size"),
ParsePaperSizeError::MissingDelimiter => write!(f, "Missing delimiter in paper size"),
}
}
}
impl FromStr for PaperSize {
type Err = ParsePaperSizeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((width, rest)) = s.split_once([',', 'x']) else {
return Err(ParsePaperSizeError::MissingDelimiter);
};
let (height, unit) = if let Some(result) = rest.split_once(',') {
result
} else if let Some(alpha) = rest.find(|c: char| c.is_alphabetic()) {
rest.split_at(alpha)
} else {
return Err(ParsePaperSizeError::MissingUnit);
};
let width = f64::from_str(width.trim()).map_err(|_| ParsePaperSizeError::InvalidWidth)?;
let height =
f64::from_str(height.trim()).map_err(|_| ParsePaperSizeError::InvalidHeight)?;
let unit = Unit::from_str(unit.trim()).map_err(|_| ParsePaperSizeError::InvalidUnit)?;
Ok(Self {
width,
height,
unit,
})
}
}
impl Display for PaperSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}x{}{}", self.width, self.height, self.unit)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ParsePaperSpecError {
InvalidHeight,
InvalidWidth,
InvalidUnit,
MissingField,
}
impl From<ParsePaperSizeError> for ParsePaperSpecError {
fn from(value: ParsePaperSizeError) -> Self {
match value {
ParsePaperSizeError::InvalidHeight => Self::InvalidHeight,
ParsePaperSizeError::InvalidWidth => Self::InvalidWidth,
ParsePaperSizeError::InvalidUnit => Self::InvalidUnit,
ParsePaperSizeError::MissingUnit => Self::MissingField,
ParsePaperSizeError::MissingDelimiter => Self::MissingField,
}
}
}
impl Error for ParsePaperSpecError {}
impl Display for ParsePaperSpecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParsePaperSpecError::InvalidHeight => write!(f, "Invalid paper height."),
ParsePaperSpecError::InvalidWidth => write!(f, "Invalid paper width."),
ParsePaperSpecError::InvalidUnit => write!(f, "Invalid unit of measurement."),
ParsePaperSpecError::MissingField => write!(f, "Missing field in paper specification."),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PaperSpec {
pub name: Cow<'static, str>,
pub size: PaperSize,
}
impl PaperSpec {
pub fn new(name: impl Into<Cow<'static, str>>, size: PaperSize) -> Self {
Self {
name: name.into(),
size,
}
}
}
impl FromStr for PaperSpec {
type Err = ParsePaperSpecError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, size) = s.split_once(',').ok_or(ParsePaperSpecError::MissingField)?;
Ok(Self {
name: String::from(name).into(),
size: size.parse()?,
})
}
}
#[derive(Debug)]
pub enum CatalogBuildError {
ParseError {
path: PathBuf,
line_number: usize,
error: ParsePaperSpecError,
},
IoError {
path: PathBuf,
error: std::io::Error,
},
}
impl Error for CatalogBuildError {}
impl Display for CatalogBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CatalogBuildError::ParseError {
path,
line_number,
error,
} => write!(f, "{}:{line_number}: {error}", path.display()),
CatalogBuildError::IoError { path, error } => {
write!(f, "{}: {error}", path.display())
}
}
}
}
pub struct CatalogBuilder<'a> {
papersize: Option<Option<&'a str>>,
use_locale: bool,
user_config_dir: Option<Option<&'a Path>>,
system_config_dir: Option<&'a Path>,
error_cb: Box<dyn FnMut(CatalogBuildError) + 'a>,
}
impl<'a> Default for CatalogBuilder<'a> {
fn default() -> Self {
Self {
use_locale: true,
papersize: None,
user_config_dir: None,
system_config_dir: Some(Path::new("/etc")),
error_cb: Box::new(drop),
}
}
}
fn fallback_specs() -> (Vec<PaperSpec>, PaperSpec) {
let specs = STANDARD_PAPERSPECS.into_iter().cloned().collect::<Vec<_>>();
let default = specs.first().unwrap().clone();
(specs, default)
}
fn read_specs<E>(
user_config_dir: Option<&Path>,
system_config_dir: Option<&Path>,
mut error_cb: E,
) -> Option<(Vec<PaperSpec>, PaperSpec)>
where
E: FnMut(CatalogBuildError),
{
fn read_paperspecs_file(
directory: Option<&Path>,
error_cb: &mut dyn FnMut(CatalogBuildError),
) -> Vec<PaperSpec> {
let mut specs = Vec::new();
if let Some(directory) = directory {
let path = directory.join(PAPERSPECS_FILENAME);
match File::open(&path) {
Ok(file) => {
let reader = BufReader::new(file);
for (line, line_number) in reader.lines().zip(1..) {
match line
.map_err(|error| CatalogBuildError::IoError {
path: path.clone(),
error,
})
.and_then(|line| {
PaperSpec::from_str(&line).map_err(|error| {
CatalogBuildError::ParseError {
path: path.clone(),
line_number,
error,
}
})
}) {
Ok(spec) => specs.push(spec),
Err(error) => error_cb(error),
}
}
}
Err(error) if error.kind() == ErrorKind::NotFound => (),
Err(error) => error_cb(CatalogBuildError::IoError { path, error }),
}
}
specs
}
let user_specs = read_paperspecs_file(user_config_dir, &mut error_cb);
let system_specs = read_paperspecs_file(system_config_dir, &mut error_cb);
let default_spec = system_specs.first().or(user_specs.first())?.clone();
Some((
user_specs.into_iter().chain(system_specs).collect(),
default_spec,
))
}
fn default_paper<E>(
papersize: Option<Option<&str>>,
user_config_dir: Option<&Path>,
_use_locale: bool,
system_config_dir: Option<&Path>,
default: &PaperSpec,
mut error_cb: E,
) -> DefaultPaper
where
E: FnMut(CatalogBuildError),
{
fn read_papersize_file<P, E>(path: P, mut error_cb: E) -> Option<String>
where
P: AsRef<Path>,
E: FnMut(CatalogBuildError),
{
fn inner(path: &Path) -> std::io::Result<Option<String>> {
let file = BufReader::new(File::open(path)?);
let line = file.lines().next().unwrap_or(Ok(String::new()))?;
let name = line.split(',').next().unwrap_or("");
Ok(name.is_empty().not().then(|| name.into()))
}
let path = path.as_ref();
match inner(path) {
Ok(result) => result,
Err(error) => {
if error.kind() != ErrorKind::NotFound {
error_cb(CatalogBuildError::IoError {
path: path.to_path_buf(),
error,
});
}
None
}
}
}
let env_var;
let paper_name = match papersize {
Some(paper_name) => paper_name,
None => {
env_var = std::env::var("PAPERSIZE").ok();
env_var.as_deref()
}
};
if let Some(paper_name) = paper_name
&& !paper_name.is_empty()
{
return DefaultPaper::Name(paper_name.into());
}
if let Some(dir) = user_config_dir
&& let path = dir.join(PAPERSIZE_FILENAME)
&& let Some(paper_name) = read_papersize_file(path, &mut error_cb)
{
return DefaultPaper::Name(paper_name);
}
#[cfg(target_os = "linux")]
if _use_locale && let Some(paper_size) = locale::locale_paper_size() {
return DefaultPaper::Size(paper_size);
}
if let Some(system_config_dir) = system_config_dir
&& let Some(paper_name) =
read_papersize_file(system_config_dir.join(PAPERSIZE_FILENAME), &mut error_cb)
{
return DefaultPaper::Name(paper_name);
}
DefaultPaper::Name(default.name.as_ref().into())
}
impl<'a> CatalogBuilder<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn build(self) -> Catalog {
self.build_inner(|user_config_dir, system_config_dir, error_cb| {
Some(
read_specs(user_config_dir, system_config_dir, error_cb)
.unwrap_or_else(fallback_specs),
)
})
.unwrap()
}
pub fn build_from_fallback(self) -> Catalog {
self.build_inner(|_, _, _| Some(fallback_specs())).unwrap()
}
pub fn build_without_fallback(self) -> Option<Catalog> {
self.build_inner(|user_config_dir, system_config_dir, error_cb| {
read_specs(user_config_dir, system_config_dir, error_cb)
})
}
pub fn with_papersize_value(self, papersize: Option<&'a str>) -> Self {
Self {
papersize: Some(papersize),
..self
}
}
pub fn without_locale(self) -> Self {
Self {
use_locale: false,
..self
}
}
pub fn with_user_config_dir(self, user_config_dir: Option<&'a Path>) -> Self {
Self {
user_config_dir: Some(user_config_dir),
..self
}
}
pub fn with_system_config_dir(self, system_config_dir: Option<&'a Path>) -> Self {
Self {
system_config_dir,
..self
}
}
pub fn with_error_callback(self, error_cb: Box<dyn FnMut(CatalogBuildError) + 'a>) -> Self {
Self { error_cb, ..self }
}
fn build_inner<F>(mut self, f: F) -> Option<Catalog>
where
F: Fn(
Option<&Path>,
Option<&Path>,
&mut Box<dyn FnMut(CatalogBuildError) + 'a>,
) -> Option<(Vec<PaperSpec>, PaperSpec)>,
{
let base_directories;
let user_config_dir = match self.user_config_dir {
Some(user_config_dir) => user_config_dir,
None => {
base_directories = BaseDirectories::new();
base_directories.config_home.as_deref()
}
};
let (specs, default) = f(user_config_dir, self.system_config_dir, &mut self.error_cb)?;
let default = match default_paper(
self.papersize,
user_config_dir,
self.use_locale,
self.system_config_dir,
&default,
&mut self.error_cb,
) {
DefaultPaper::Name(name) => specs
.iter()
.find(|spec| spec.name.eq_ignore_ascii_case(&name))
.cloned()
.unwrap_or(default),
DefaultPaper::Size(size) => specs
.iter()
.find(|spec| spec.size.eq_rounded(&size, Unit::Point))
.cloned()
.unwrap_or_else(|| PaperSpec::new(Cow::from("Locale"), size)),
};
Some(Catalog { specs, default })
}
}
pub struct Catalog {
specs: Vec<PaperSpec>,
default: PaperSpec,
}
impl Default for Catalog {
fn default() -> Self {
Self::builder().build()
}
}
impl Catalog {
pub fn builder<'a>() -> CatalogBuilder<'a> {
CatalogBuilder::new()
}
pub fn new() -> Self {
Self::default()
}
pub fn specs(&self) -> &[PaperSpec] {
&self.specs
}
pub fn default_paper(&self) -> &PaperSpec {
&self.default
}
pub fn get_by_size(&self, size: &PaperSize) -> Option<&PaperSpec> {
self.specs
.iter()
.find(|spec| spec.size.eq_rounded(size, Unit::Point))
}
pub fn get_by_name(&self, name: &str) -> Option<&PaperSpec> {
self.specs
.iter()
.find(|spec| spec.name.eq_ignore_ascii_case(name))
}
}
#[cfg(test)]
mod tests {
use std::{borrow::Cow, path::Path, str::FromStr};
use crate::{
A4, CatalogBuildError, CatalogBuilder, Length, PaperSize, PaperSpec, ParsePaperSizeError,
ParsePaperSpecError, Unit, locale,
};
#[test]
fn unit() {
assert_eq!(Unit::Point.to_string(), "pt");
assert_eq!(Unit::Millimeter.to_string(), "mm");
assert_eq!(Unit::Inch.to_string(), "in");
assert_eq!("pt".parse(), Ok(Unit::Point));
assert_eq!("mm".parse(), Ok(Unit::Millimeter));
assert_eq!("in".parse(), Ok(Unit::Inch));
assert_eq!(
format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Millimeter)),
"25.400"
);
assert_eq!(
format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Inch)),
"1.000"
);
assert_eq!(
format!("{:.3}", 1.0 * Unit::Inch.as_unit(Unit::Point)),
"72.000"
);
assert_eq!(
format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Millimeter)),
"12.700"
);
assert_eq!(
format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Inch)),
"0.500"
);
assert_eq!(
format!("{:.3}", 36.0 * Unit::Point.as_unit(Unit::Point)),
"36.000"
);
assert_eq!(
format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Millimeter)),
"12.700"
);
assert_eq!(
format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Inch)),
"0.500"
);
assert_eq!(
format!("{:.3}", 12.7 * Unit::Millimeter.as_unit(Unit::Point)),
"36.000"
);
}
#[test]
fn length() {
assert_eq!(
format!(
"{:.3}",
Length::new(1.0, Unit::Inch).into_unit(Unit::Millimeter)
),
"25.400"
);
assert_eq!(
format!("{:.3}", Length::new(1.0, Unit::Inch).into_unit(Unit::Inch)),
"1.000"
);
assert_eq!(
format!("{:.3}", Length::new(1.0, Unit::Inch).into_unit(Unit::Point)),
"72.000"
);
assert_eq!(
format!(
"{:.3}",
Length::new(36.0, Unit::Point).into_unit(Unit::Millimeter)
),
"12.700"
);
assert_eq!(
format!(
"{:.3}",
Length::new(36.0, Unit::Point).into_unit(Unit::Inch)
),
"0.500"
);
assert_eq!(
format!(
"{:.3}",
Length::new(36.0, Unit::Point).into_unit(Unit::Point)
),
"36.000"
);
assert_eq!(
format!(
"{:.3}",
Length::new(12.7, Unit::Millimeter).into_unit(Unit::Millimeter)
),
"12.700"
);
assert_eq!(
format!(
"{:.3}",
Length::new(12.7, Unit::Millimeter).into_unit(Unit::Inch)
),
"0.500"
);
assert_eq!(
format!(
"{:.3}",
Length::new(12.7, Unit::Millimeter).into_unit(Unit::Point)
),
"36.000"
);
}
#[test]
fn papersize() {
assert_eq!(
"8.5x11in".parse(),
Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
);
assert_eq!(
"8.5,11in".parse(),
Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
);
assert_eq!(
" 8.5 x 11 in ".parse(),
Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
);
assert_eq!(
PaperSize::from_str("8.5x.in"),
Err(ParsePaperSizeError::InvalidHeight)
);
assert_eq!(
PaperSize::from_str(".x11in"),
Err(ParsePaperSizeError::InvalidWidth)
);
assert_eq!(
PaperSize::from_str("8.5x11xyzzy"),
Err(ParsePaperSizeError::InvalidUnit)
);
assert_eq!(
PaperSize::from_str("8.5x11"),
Err(ParsePaperSizeError::MissingUnit)
);
assert_eq!(
PaperSize::from_str(" 8.5 11 in "),
Err(ParsePaperSizeError::MissingDelimiter)
);
assert_eq!(
PaperSize::new(8.5, 11.0, Unit::Inch).to_string(),
"8.5x11in"
);
assert_eq!(A4.size.to_string(), "210x297mm");
}
#[test]
fn paperspec() {
assert_eq!(
"Letter,8.5,11,in".parse(),
Ok(PaperSpec::new(
Cow::from("Letter"),
PaperSize::new(8.5, 11.0, Unit::Inch)
))
);
assert_eq!(
"Letter,8.5x11in".parse(),
Ok(PaperSpec::new(
Cow::from("Letter"),
PaperSize::new(8.5, 11.0, Unit::Inch)
))
);
}
#[test]
fn default() {
assert_eq!(
CatalogBuilder::new()
.with_papersize_value(Some("legal"))
.with_user_config_dir(Some(Path::new("testdata/td1")))
.without_locale()
.build_from_fallback()
.default_paper(),
&PaperSpec::new(Cow::from("Legal"), PaperSize::new(8.5, 14.0, Unit::Inch))
);
assert_eq!(
CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(Some(Path::new("testdata/td1")))
.without_locale()
.build_from_fallback()
.default_paper(),
&PaperSpec::new(Cow::from("Ledger"), PaperSize::new(17.0, 11.0, Unit::Inch))
);
assert_eq!(
CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(None)
.with_system_config_dir(Some(Path::new("testdata/td2")))
.without_locale()
.build_from_fallback()
.default_paper(),
&PaperSpec::new(
Cow::from("Executive"),
PaperSize::new(7.25, 10.5, Unit::Inch)
)
);
assert_eq!(
CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(None)
.with_system_config_dir(Some(Path::new("testdata/td2")))
.without_locale()
.build()
.default_paper(),
&PaperSpec::new(
Cow::from("A0"),
PaperSize::new(841.0, 1189.0, Unit::Millimeter)
)
);
assert_eq!(
CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(Some(Path::new("testdata/td3")))
.with_system_config_dir(None)
.without_locale()
.build()
.default_paper(),
&PaperSpec::new(
Cow::from("B0"),
PaperSize::new(1000.0, 1414.0, Unit::Millimeter)
)
);
assert_eq!(
CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(None)
.with_system_config_dir(None)
.without_locale()
.build()
.default_paper(),
&PaperSpec::new(
Cow::from("A4"),
PaperSize::new(210.0, 297.0, Unit::Millimeter)
)
);
assert!(
CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(None)
.with_system_config_dir(None)
.without_locale()
.build_without_fallback()
.is_none()
);
}
#[test]
fn errors() {
let mut errors = Vec::new();
let _ = CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(Some(Path::new("nonexistent/user")))
.with_system_config_dir(Some(Path::new("nonexistent/system")))
.without_locale()
.with_error_callback(Box::new(|error| errors.push(error)))
.build()
.default_paper();
assert_eq!(errors.len(), 0);
let mut errors = Vec::new();
let _ = CatalogBuilder::new()
.with_papersize_value(None)
.with_user_config_dir(None)
.with_system_config_dir(Some(Path::new("testdata/td4")))
.without_locale()
.with_error_callback(Box::new(|error| errors.push(error)))
.build()
.default_paper();
assert_eq!(errors.len(), 4);
for ((error, expect_line_number), expect_error) in errors.iter().zip(1..).zip([
ParsePaperSpecError::MissingField,
ParsePaperSpecError::InvalidWidth,
ParsePaperSpecError::InvalidHeight,
ParsePaperSpecError::InvalidUnit,
]) {
let CatalogBuildError::ParseError {
path,
line_number,
error,
} = error
else {
unreachable!()
};
assert_eq!(path.as_path(), Path::new("testdata/td4/paperspecs"));
assert_eq!(*line_number, expect_line_number);
assert_eq!(*error, expect_error);
}
}
#[cfg(target_os = "linux")]
#[test]
fn lc_paper() {
if let Some(size) = locale::locale_paper_size() {
assert_eq!(size.unit, Unit::Millimeter);
let (w, h) = size.into_width_height();
assert!(
(w, h) == (210.0, 297.0) || (w, h) == (216.0, 279.0),
"Expected A4 (210x297) or letter (216x279) paper, got {w}x{h} mm"
);
}
}
}