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}