Skip to main content

car/
output.rs

1use std::path::{Component, Path, PathBuf};
2
3use crate::car::{CSIItem, Car};
4use crate::model::Encoding;
5use crate::model::rendition::Rendition;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum OutputIdentityKind {
9    CanonicalSourceOutput,
10    VariantOutput,
11}
12
13#[derive(Debug, Clone)]
14pub struct OutputIdentity<'a> {
15    kind: OutputIdentityKind,
16    facet_name: String,
17    item: &'a CSIItem,
18}
19
20impl<'a> OutputIdentity<'a> {
21    pub fn canonical_source(facet_name: impl Into<String>, item: &'a CSIItem) -> Self {
22        Self {
23            kind: OutputIdentityKind::CanonicalSourceOutput,
24            facet_name: facet_name.into(),
25            item,
26        }
27    }
28
29    pub fn variant(facet_name: impl Into<String>, item: &'a CSIItem) -> Self {
30        Self {
31            kind: OutputIdentityKind::VariantOutput,
32            facet_name: facet_name.into(),
33            item,
34        }
35    }
36
37    pub fn kind(&self) -> OutputIdentityKind {
38        self.kind
39    }
40
41    pub fn facet_name(&self) -> &str {
42        &self.facet_name
43    }
44
45    pub fn item(&self) -> &'a CSIItem {
46        self.item
47    }
48
49    pub fn include_scale_suffix(&self) -> bool {
50        matches!(self.kind, OutputIdentityKind::VariantOutput)
51    }
52
53    pub fn canonical_identity_key(&self) -> Option<Vec<u16>> {
54        matches!(self.kind, OutputIdentityKind::CanonicalSourceOutput)
55            .then(|| self.item.key_values().to_vec())
56    }
57}
58
59impl Car {
60    pub fn output_identity_for_payload<'a>(
61        &'a self,
62        logical_facet_name: &str,
63        logical_item: &'a CSIItem,
64        payload_item: &'a CSIItem,
65    ) -> Option<OutputIdentity<'a>> {
66        match supported_output_identity(payload_item)? {
67            OutputIdentityKind::CanonicalSourceOutput => {
68                let (facet_name, item) = self
69                    .find_canonical_named_item(payload_item)
70                    .unwrap_or((logical_facet_name.to_string(), payload_item));
71                Some(OutputIdentity::canonical_source(facet_name, item))
72            }
73            OutputIdentityKind::VariantOutput => {
74                Some(OutputIdentity::variant(logical_facet_name, logical_item))
75            }
76        }
77    }
78
79    fn find_canonical_named_item<'a>(
80        &'a self,
81        payload_item: &'a CSIItem,
82    ) -> Option<(String, &'a CSIItem)> {
83        let key_values = payload_item.key_values();
84        for (facet_name, facet) in self.named_facets() {
85            let Some(items) = self.items_for_facet(facet) else {
86                continue;
87            };
88            if let Some(item) = items.iter().find(|item| {
89                item.key_values() == key_values && supported_output_identity(item).is_some()
90            }) {
91                return Some((facet_name.to_string(), item));
92            }
93        }
94        None
95    }
96}
97
98pub fn supported_output_identity(item: &CSIItem) -> Option<OutputIdentityKind> {
99    match &item.header().rendition {
100        Some(Rendition::RawData(_))
101            if matches!(
102                item.header().encoding,
103                Encoding::Data | Encoding::PDF | Encoding::SVG
104            ) =>
105        {
106            Some(OutputIdentityKind::CanonicalSourceOutput)
107        }
108        Some(Rendition::Color(_))
109        | Some(Rendition::ThemeCBCK(_))
110        | Some(Rendition::MultisizeImageSet(_))
111        | Some(Rendition::RawData(_)) => Some(OutputIdentityKind::VariantOutput),
112        _ => None,
113    }
114}
115
116pub fn rendition_scale(item: &CSIItem) -> u32 {
117    item.attributes()
118        .iter()
119        .find(|attr| attr.name == crate::rendition::AttributeType::Scale && attr.val > 0)
120        .map(|attr| u32::from(attr.val))
121        .unwrap_or_else(|| item.scale())
122}
123
124pub fn suggested_file_name(identity: &OutputIdentity<'_>, extension: &str) -> Option<String> {
125    let mut file_name = sanitize_file_name(identity.item().name())?;
126
127    let scale = rendition_scale(identity.item());
128    if identity.include_scale_suffix() && scale > 1 {
129        let stem = file_name
130            .file_stem()
131            .unwrap_or_default()
132            .to_string_lossy()
133            .to_string();
134        if !stem.ends_with(&format!("@{}x", scale)) {
135            let current_ext = file_name
136                .extension()
137                .map(|ext| ext.to_string_lossy().to_string());
138            let renamed = match current_ext {
139                Some(current_ext) => format!("{stem}@{scale}x.{current_ext}"),
140                None => format!("{stem}@{scale}x"),
141            };
142            file_name = PathBuf::from(renamed);
143        }
144    }
145
146    if !extension.is_empty() {
147        let has_expected_extension = file_name
148            .extension()
149            .and_then(|ext| ext.to_str())
150            .is_some_and(|ext| ext.eq_ignore_ascii_case(extension));
151        if !has_expected_extension {
152            file_name.set_extension(extension);
153        }
154    }
155
156    file_name
157        .file_name()
158        .map(|name| name.to_string_lossy().into_owned())
159}
160
161pub fn default_raw_extension(encoding: Encoding) -> &'static str {
162    match encoding {
163        Encoding::HEIF => "heic",
164        Encoding::PDF => "pdf",
165        Encoding::SVG => "svg",
166        Encoding::JPEG => "jpg",
167        Encoding::WEBP => "webp",
168        Encoding::Data | Encoding::None => "bin",
169        _ => "bin",
170    }
171}
172
173pub fn default_raw_extension_for_item(item: &CSIItem) -> &'static str {
174    if crate::image_conv::is_hevc_compressed(item) {
175        "heic"
176    } else {
177        default_raw_extension(item.header().encoding)
178    }
179}
180
181pub(crate) fn sanitize_file_name(name: &str) -> Option<PathBuf> {
182    let mut file_name = None;
183
184    for component in Path::new(name).components() {
185        match component {
186            Component::Normal(part) => file_name = Some(PathBuf::from(part)),
187            Component::CurDir => {}
188            Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
189        }
190    }
191
192    file_name
193}