Skip to main content

car/
asset.rs

1//! High-level asset model and typed variant queries.
2
3use std::cmp::Ordering;
4
5use thiserror::Error;
6
7use crate::car::{CSIItem, Car, ReferenceRect};
8use crate::metadata::TlvMetadata;
9use crate::model::rendition::{AttributeType, CompressionType, LayoutType, Rendition};
10use crate::model::{ColorModel, Encoding};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
13pub struct AssetId(u32);
14
15impl AssetId {
16    pub fn new(index: u32) -> Self {
17        Self(index)
18    }
19
20    pub fn index(self) -> u32 {
21        self.0
22    }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum AssetKind {
27    Image,
28    Color,
29    Document,
30    RawData,
31    MultisizeImageSet,
32    Unknown,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum PayloadKind {
37    Original,
38    DecodedRaster,
39    ColorMetadata,
40    Dispatch,
41    Unsupported,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct TypedAttribute {
46    pub name: AttributeType,
47    pub value: u16,
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct VariantAttributes {
52    pub scale: Option<u16>,
53    pub idiom: Option<u16>,
54    pub display_gamut: Option<u16>,
55    pub appearance: Option<u16>,
56    pub localization: Option<u16>,
57    pub horizontal_size_class: Option<u16>,
58    pub vertical_size_class: Option<u16>,
59    pub all: Vec<TypedAttribute>,
60    pub unknown: Vec<TypedAttribute>,
61}
62
63pub type AttributeSet = VariantAttributes;
64
65impl VariantAttributes {
66    pub fn raw(&self, name: AttributeType) -> Option<u16> {
67        self.all
68            .iter()
69            .find(|attribute| attribute.name == name)
70            .map(|attribute| attribute.value)
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct ResolvedPayloadSummary {
76    pub rendition_name: String,
77    pub kind: AssetKind,
78    pub payload_kind: PayloadKind,
79    pub width: u32,
80    pub height: u32,
81    pub scale: u32,
82    pub layout: LayoutType,
83    pub encoding: Encoding,
84    pub color_model: ColorModel,
85    pub compression: Option<CompressionType>,
86    pub key_values: Vec<u16>,
87    pub effective_layout: u16,
88    pub crops: Vec<ReferenceRect>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct AssetEntry {
93    pub id: AssetId,
94    pub facet_name: String,
95    pub rendition_name: String,
96    pub kind: AssetKind,
97    pub payload_kind: PayloadKind,
98    pub width: u32,
99    pub height: u32,
100    pub scale: u32,
101    pub layout: LayoutType,
102    pub encoding: Encoding,
103    pub color_model: ColorModel,
104    pub compression: Option<CompressionType>,
105    pub attributes: VariantAttributes,
106    pub metadata: TlvMetadata,
107    pub variant_count: usize,
108    pub resolved_payload: Option<ResolvedPayloadSummary>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct AssetVariant {
113    pub asset_id: AssetId,
114    pub facet_name: String,
115    pub rendition_name: String,
116    pub kind: AssetKind,
117    pub payload_kind: PayloadKind,
118    pub width: u32,
119    pub height: u32,
120    pub scale: u32,
121    pub layout: LayoutType,
122    pub encoding: Encoding,
123    pub compression: Option<CompressionType>,
124    pub attributes: VariantAttributes,
125    pub key_values: Vec<u16>,
126}
127
128#[derive(Debug, Clone, Default, PartialEq, Eq)]
129pub struct VariantQuery {
130    pub scale: Option<u16>,
131    pub idiom: Option<u16>,
132    pub display_gamut: Option<u16>,
133    pub appearance: Option<u16>,
134    pub localization: Option<u16>,
135    pub horizontal_size_class: Option<u16>,
136    pub vertical_size_class: Option<u16>,
137}
138
139impl VariantQuery {
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    pub fn scale(mut self, scale: u16) -> Self {
145        self.scale = Some(scale);
146        self
147    }
148
149    pub fn idiom(mut self, idiom: u16) -> Self {
150        self.idiom = Some(idiom);
151        self
152    }
153
154    pub fn display_gamut(mut self, display_gamut: u16) -> Self {
155        self.display_gamut = Some(display_gamut);
156        self
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Error)]
161pub enum VariantMatchError {
162    #[error("asset `{name}` was not found")]
163    AssetNotFound { name: String },
164    #[error("asset `{name}` has no variants")]
165    NoVariants { name: String },
166    #[error("asset `{name}` has no variant matching {query:?}")]
167    NoMatch {
168        name: String,
169        query: VariantQuery,
170        available: Vec<VariantAttributes>,
171    },
172}
173
174impl CSIItem {
175    pub fn variant_attributes(&self) -> VariantAttributes {
176        let mut result = VariantAttributes::default();
177
178        for attr in self.attributes() {
179            let typed = TypedAttribute {
180                name: attr.name,
181                value: attr.val,
182            };
183            match attr.name {
184                AttributeType::Scale => result.scale = Some(attr.val),
185                AttributeType::Idiom => result.idiom = Some(attr.val),
186                AttributeType::DisplayGamut => result.display_gamut = Some(attr.val),
187                AttributeType::ThemeAppearance => result.appearance = Some(attr.val),
188                AttributeType::Localization => result.localization = Some(attr.val),
189                AttributeType::HorizontalSizeClass => result.horizontal_size_class = Some(attr.val),
190                AttributeType::VerticalSizeClass => result.vertical_size_class = Some(attr.val),
191                AttributeType::Unknown { .. } => result.unknown.push(typed.clone()),
192                _ => {}
193            }
194            result.all.push(typed);
195        }
196
197        result
198    }
199}
200
201struct LocatedAsset<'a> {
202    id: AssetId,
203    facet_name: &'a str,
204    item: &'a CSIItem,
205    facet_items: &'a [CSIItem],
206    facet_start_id: u32,
207}
208
209impl Car {
210    pub fn entries(&self) -> Vec<AssetEntry> {
211        let mut entries = Vec::new();
212        for (facet_name, facet) in self.named_facets() {
213            let Some(items) = self.items_for_facet(facet) else {
214                continue;
215            };
216            let variant_count = items.len();
217            for item in items {
218                entries.push(build_entry(
219                    AssetId::new(entries.len() as u32),
220                    facet_name,
221                    item,
222                    variant_count,
223                    self,
224                ));
225            }
226        }
227        entries
228    }
229
230    pub fn entry(&self, id: AssetId) -> Option<AssetEntry> {
231        let located = self.locate_asset(id)?;
232        Some(build_entry(
233            located.id,
234            located.facet_name,
235            located.item,
236            located.facet_items.len(),
237            self,
238        ))
239    }
240
241    pub fn variants_for(&self, id: AssetId) -> Option<Vec<AssetVariant>> {
242        let located = self.locate_asset(id)?;
243        Some(
244            located
245                .facet_items
246                .iter()
247                .enumerate()
248                .map(|(offset, item)| {
249                    build_variant(
250                        AssetId::new(located.facet_start_id.wrapping_add(offset as u32)),
251                        located.facet_name,
252                        item,
253                    )
254                })
255                .collect(),
256        )
257    }
258
259    pub fn variants_for_name(&self, name: &str) -> Option<Vec<AssetVariant>> {
260        let mut variants = Vec::new();
261        let mut next_id = 0u32;
262
263        for (facet_name, facet) in self.named_facets() {
264            let Some(items) = self.items_for_facet(facet) else {
265                continue;
266            };
267            for item in items {
268                let id = AssetId::new(next_id);
269                next_id = next_id.wrapping_add(1);
270                if facet_name == name {
271                    variants.push(build_variant(id, facet_name, item));
272                }
273            }
274        }
275
276        (!variants.is_empty()).then_some(variants)
277    }
278
279    pub fn best_variant_for_name(
280        &self,
281        name: &str,
282        query: &VariantQuery,
283    ) -> Result<AssetVariant, VariantMatchError> {
284        let variants =
285            self.variants_for_name(name)
286                .ok_or_else(|| VariantMatchError::AssetNotFound {
287                    name: name.to_string(),
288                })?;
289        if variants.is_empty() {
290            return Err(VariantMatchError::NoVariants {
291                name: name.to_string(),
292            });
293        }
294
295        if let Some(best) = variants
296            .iter()
297            .filter(|variant| matches_query(&variant.attributes, query))
298            .min_by(|lhs, rhs| compare_variants(lhs, rhs, query))
299            .cloned()
300        {
301            Ok(best)
302        } else {
303            Err(VariantMatchError::NoMatch {
304                name: name.to_string(),
305                query: query.clone(),
306                available: variants
307                    .into_iter()
308                    .map(|variant| variant.attributes)
309                    .collect(),
310            })
311        }
312    }
313
314    fn locate_asset(&self, id: AssetId) -> Option<LocatedAsset<'_>> {
315        let mut next_id = 0u32;
316
317        for (facet_name, facet) in self.named_facets() {
318            let Some(items) = self.items_for_facet(facet) else {
319                continue;
320            };
321            let facet_start_id = next_id;
322            for item in items {
323                let current_id = AssetId::new(next_id);
324                next_id = next_id.wrapping_add(1);
325                if current_id == id {
326                    return Some(LocatedAsset {
327                        id: current_id,
328                        facet_name,
329                        item,
330                        facet_items: items,
331                        facet_start_id,
332                    });
333                }
334            }
335        }
336
337        None
338    }
339}
340
341fn build_entry(
342    id: AssetId,
343    facet_name: &str,
344    item: &CSIItem,
345    variant_count: usize,
346    car: &Car,
347) -> AssetEntry {
348    let resolved_payload = car
349        .try_resolve_internal_reference(item)
350        .ok()
351        .map(|resolved| ResolvedPayloadSummary {
352            rendition_name: resolved.source.name().to_string(),
353            kind: asset_kind_for_item(resolved.source),
354            payload_kind: payload_kind_for_item(resolved.source),
355            width: resolved.source.width(),
356            height: resolved.source.height(),
357            scale: resolved.source.scale(),
358            layout: resolved.source.layout(),
359            encoding: resolved.source.encoding(),
360            color_model: resolved.source.color_model(),
361            compression: resolved.source.compression(),
362            key_values: resolved.source.key_values().to_vec(),
363            effective_layout: resolved.effective_layout,
364            crops: resolved.crops,
365        });
366
367    AssetEntry {
368        id,
369        facet_name: facet_name.to_string(),
370        rendition_name: item.name().to_string(),
371        kind: asset_kind_for_item(item),
372        payload_kind: payload_kind_for_item(item),
373        width: item.width(),
374        height: item.height(),
375        scale: item.scale(),
376        layout: item.layout(),
377        encoding: item.encoding(),
378        color_model: item.color_model(),
379        compression: item.compression(),
380        attributes: item.variant_attributes(),
381        metadata: item.tlv_metadata(),
382        variant_count,
383        resolved_payload,
384    }
385}
386
387fn build_variant(id: AssetId, facet_name: &str, item: &CSIItem) -> AssetVariant {
388    AssetVariant {
389        asset_id: id,
390        facet_name: facet_name.to_string(),
391        rendition_name: item.name().to_string(),
392        kind: asset_kind_for_item(item),
393        payload_kind: payload_kind_for_item(item),
394        width: item.width(),
395        height: item.height(),
396        scale: item.scale(),
397        layout: item.layout(),
398        encoding: item.encoding(),
399        compression: item.compression(),
400        attributes: item.variant_attributes(),
401        key_values: item.key_values().to_vec(),
402    }
403}
404
405pub fn asset_kind_for_item(item: &CSIItem) -> AssetKind {
406    match item.header().rendition.as_ref() {
407        Some(Rendition::Color(_)) => AssetKind::Color,
408        Some(Rendition::RawData(_)) => match item.encoding() {
409            Encoding::PDF | Encoding::SVG => AssetKind::Document,
410            _ => AssetKind::RawData,
411        },
412        Some(Rendition::ThemeCBCK(_)) => match item.encoding() {
413            Encoding::PDF | Encoding::SVG => AssetKind::Document,
414            _ => AssetKind::Image,
415        },
416        Some(Rendition::MultisizeImageSet(_)) => AssetKind::MultisizeImageSet,
417        Some(Rendition::Unknown { .. }) | None => AssetKind::Unknown,
418    }
419}
420
421pub fn payload_kind_for_item(item: &CSIItem) -> PayloadKind {
422    if matches!(item.layout(), LayoutType::InternalReference) {
423        return PayloadKind::Dispatch;
424    }
425
426    match item.header().rendition.as_ref() {
427        Some(Rendition::Color(_)) => PayloadKind::ColorMetadata,
428        Some(Rendition::RawData(_)) => PayloadKind::Original,
429        Some(Rendition::ThemeCBCK(_)) => match item.encoding() {
430            Encoding::ARGB
431            | Encoding::ARGB16
432            | Encoding::GRAY
433            | Encoding::GA16
434            | Encoding::GA8
435            | Encoding::RGB5 => PayloadKind::DecodedRaster,
436            Encoding::JPEG | Encoding::WEBP | Encoding::HEIF | Encoding::PDF | Encoding::SVG => {
437                PayloadKind::Original
438            }
439            _ => PayloadKind::Unsupported,
440        },
441        Some(Rendition::MultisizeImageSet(_)) => PayloadKind::Original,
442        Some(Rendition::Unknown { .. }) | None => PayloadKind::Unsupported,
443    }
444}
445
446fn matches_query(attributes: &VariantAttributes, query: &VariantQuery) -> bool {
447    optional_eq(attributes.scale, query.scale)
448        && optional_eq(attributes.idiom, query.idiom)
449        && optional_eq(attributes.display_gamut, query.display_gamut)
450        && optional_eq(attributes.appearance, query.appearance)
451        && optional_eq(attributes.localization, query.localization)
452        && optional_eq(
453            attributes.horizontal_size_class,
454            query.horizontal_size_class,
455        )
456        && optional_eq(attributes.vertical_size_class, query.vertical_size_class)
457}
458
459fn optional_eq(actual: Option<u16>, expected: Option<u16>) -> bool {
460    expected.is_none_or(|expected| actual == Some(expected))
461}
462
463fn compare_variants(lhs: &AssetVariant, rhs: &AssetVariant, query: &VariantQuery) -> Ordering {
464    let lhs_scale = scale_rank(lhs, query.scale);
465    let rhs_scale = scale_rank(rhs, query.scale);
466    lhs_scale
467        .cmp(&rhs_scale)
468        .then_with(|| lhs.rendition_name.cmp(&rhs.rendition_name))
469        .then_with(|| lhs.key_values.cmp(&rhs.key_values))
470        .then_with(|| lhs.asset_id.cmp(&rhs.asset_id))
471}
472
473fn scale_rank(variant: &AssetVariant, requested: Option<u16>) -> (u8, u16) {
474    let scale = variant.attributes.scale.unwrap_or(variant.scale as u16);
475    match requested {
476        Some(requested) if scale == requested => (0, scale),
477        Some(requested) => (1, scale.abs_diff(requested)),
478        None if scale == 1 => (0, scale),
479        None => (1, scale),
480    }
481}