1use 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}