use crate::error::Error;
use std::{fmt, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilmFormat {
IX240,
F135,
F135Half,
F16,
F645,
F66,
F67,
F68,
F69,
Custom(u32),
}
impl FilmFormat {
const fn height_tenths(self) -> u32 {
match self {
Self::IX240 => 302,
Self::F135 => 360,
Self::F135Half => 180,
Self::F16 => 200,
Self::F645 => 415,
Self::F66 => 560,
Self::F67 => 695,
Self::F68 => 760,
Self::F69 => 840,
Self::Custom(mm) => mm * 10,
}
}
pub const fn height_mm(self) -> u32 {
(self.height_tenths() + 5) / 10
}
pub fn height_dots(self, dpi: u16) -> u32 {
let num = u64::from(self.height_tenths()) * u64::from(dpi);
((num + 127) / 254) as u32
}
pub fn from_holder(holder_id: u8) -> Option<Self> {
match holder_id {
0x12 => Some(Self::F16), 0x19 => Some(Self::F645), 0x1A => Some(Self::F66), 0x1B => Some(Self::F67), 0x1C => Some(Self::F68), 0x1D => Some(Self::F69), _ => None,
}
}
pub fn choices_for_holder(holder_id: u8) -> Option<&'static [Self]> {
match holder_id {
0x14 => Some(&[Self::F66, Self::F67, Self::F69]), 0x15 => Some(&[Self::F66, Self::F67, Self::F69]), 0x16 => Some(&[Self::F66, Self::F67, Self::F69]), 0x17 => Some(&[Self::F66, Self::F67, Self::F69]), 0x18 => Some(&[Self::F66, Self::F67, Self::F69]), _ => None,
}
}
pub fn from_adapter(adapter_id: u8) -> Option<Self> {
match adapter_id {
0x31 => Some(Self::F135), 0x35 => Some(Self::IX240), 0x32 => Some(Self::F135), _ => None,
}
}
pub fn resolve(explicit: Option<Self>, caps: &super::Capabilities) -> Result<Self, Error> {
if let Some(format) = explicit {
return Ok(format);
}
let uses_adapter =
!caps.identity.is_mf_scanner() && caps.address.adapter_id.is_some_and(|id| id > 0);
let id = if uses_adapter {
caps.address.connected_adapter
} else {
caps.address.holder_id
}
.ok_or_else(|| Error::Unsupported {
op: "film format",
reason: "no holder loaded; supply a format".into(),
})?;
Self::from_holder(id)
.or_else(|| Self::from_adapter(id))
.ok_or_else(|| {
let choices = Self::choices_for_holder(id)
.map(|c| {
format!(
" (try: {})",
c.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
})
.unwrap_or_default();
Error::Unsupported {
op: "film format",
reason: format!("this holder does not fix it{choices}"),
}
})
}
}
impl FromStr for FilmFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
Ok(match s {
"IX240" | "aps" | "APS" => Self::IX240,
"135" => Self::F135,
"half" | "135half" => Self::F135Half,
"16" => Self::F16,
"645" => Self::F645,
"66" => Self::F66,
"67" => Self::F67,
"68" => Self::F68,
"69" => Self::F69,
mm => Self::Custom(
mm.parse()
.map_err(|_| format!("'{mm}' is neither a film format nor a height in mm"))?,
),
})
}
}
impl fmt::Display for FilmFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IX240 => write!(f, "IX240"),
Self::F135 => write!(f, "135"),
Self::F135Half => write!(f, "half"),
Self::F16 => write!(f, "16"),
Self::F645 => write!(f, "645"),
Self::F66 => write!(f, "66"),
Self::F67 => write!(f, "67"),
Self::F68 => write!(f, "68"),
Self::F69 => write!(f, "69"),
Self::Custom(mm) => write!(f, "{mm}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_heights() {
assert_eq!(FilmFormat::F135.height_mm(), 36);
assert_eq!(FilmFormat::F16.height_mm(), 20);
assert_eq!(FilmFormat::F66.height_mm(), 56);
assert_eq!(FilmFormat::F69.height_mm(), 84);
assert_eq!(FilmFormat::Custom(100).height_mm(), 100);
}
#[test]
fn a_format_is_its_gate_rather_than_its_name() {
assert_eq!(FilmFormat::F645.height_tenths(), 415);
assert_eq!(FilmFormat::F67.height_tenths(), 695);
assert_eq!(FilmFormat::F68.height_tenths(), 760);
assert_eq!(
FilmFormat::F135Half.height_tenths() * 2,
FilmFormat::F135.height_tenths()
);
}
#[test]
fn dots_at_4000_dpi() {
assert_eq!(FilmFormat::F66.height_dots(4000), 8819);
assert_eq!(FilmFormat::F69.height_dots(4000), 13228);
assert_eq!(FilmFormat::F645.height_dots(4000), 6535);
}
#[test]
fn gr_holder_fixes_format() {
assert_eq!(FilmFormat::from_holder(0x1A), Some(FilmFormat::F66));
assert_eq!(FilmFormat::from_holder(0x1D), Some(FilmFormat::F69));
assert_eq!(FilmFormat::from_holder(0x17), None);
}
#[test]
fn strip_holder_offers_choices() {
let choices = FilmFormat::choices_for_holder(0x17).unwrap();
assert!(choices.contains(&FilmFormat::F66));
assert!(choices.contains(&FilmFormat::F69));
assert!(FilmFormat::choices_for_holder(0x1A).is_none());
}
}