Skip to main content

car/
view.rs

1//! Stable serializable view objects for reporting `.car` contents.
2//!
3//! These types power `car-cli info` and are intended for callers that want
4//! asset metadata without depending on raw CoreUI structs.
5//!
6//! Typical usage:
7//!
8//! ```rust
9//! # use car::Car;
10//! # fn demo(car: &Car) {
11//! let document = car.document_info();
12//! let renditions = car.rendition_infos();
13//! # let _ = (document, renditions);
14//! # }
15//! ```
16
17use serde::Serialize;
18
19use crate::car::{CSIItem, Car};
20use crate::model::rendition::{self, AttributeType, Idiom, LayoutType, Rendition};
21use crate::{ColorModel, ColorSpace, Encoding};
22
23#[derive(Debug, Clone, Serialize)]
24pub struct DocumentInfo {
25    #[serde(rename = "AssetStorageVersion")]
26    pub asset_storage_version: u32,
27    #[serde(rename = "CoreUIVersion")]
28    pub core_ui_version: u32,
29    #[serde(rename = "SchemaVersion")]
30    pub schema_version: u32,
31    #[serde(rename = "Timestamp")]
32    pub timestamp: u32,
33    #[serde(rename = "MainVersionString")]
34    pub main_version_string: String,
35    #[serde(rename = "VersionString")]
36    pub version_string: String,
37    #[serde(rename = "UUID")]
38    pub uuid: String,
39    #[serde(rename = "ColorSpace")]
40    pub color_space: &'static str,
41    #[serde(rename = "KeySemantics")]
42    pub key_semantics: u32,
43    #[serde(rename = "RenditionCount")]
44    pub rendition_count: u32,
45    #[serde(rename = "AuthoringTool")]
46    pub authoring_tool: String,
47    #[serde(rename = "DeploymentPlatform")]
48    pub deployment_platform: String,
49    #[serde(rename = "DeploymentPlatformVersion")]
50    pub deployment_platform_version: String,
51    #[serde(rename = "ThinningArguments")]
52    pub thinning_arguments: String,
53}
54
55#[derive(Debug, Clone, Serialize)]
56pub struct ResolvedPayloadInfo {
57    #[serde(rename = "Encoding")]
58    pub encoding: String,
59    #[serde(rename = "ColorModel")]
60    pub color_model: &'static str,
61    #[serde(rename = "Opaque")]
62    pub opaque: bool,
63    #[serde(rename = "VectorBased")]
64    pub vector_based: bool,
65    #[serde(rename = "Flippable")]
66    pub flippable: bool,
67    #[serde(rename = "Tintable")]
68    pub tintable: bool,
69    #[serde(rename = "AssetType")]
70    pub asset_type: &'static str,
71    #[serde(
72        rename = "Compression",
73        skip_serializing_if = "Option::is_none",
74        default
75    )]
76    pub compression: Option<&'static str>,
77}
78
79#[derive(Debug, Clone, Serialize)]
80#[serde(untagged)]
81pub enum AttributeValue {
82    String(String),
83    Number(u16),
84}
85
86#[derive(Debug, Clone, Serialize)]
87pub struct RenditionInfo {
88    #[serde(rename = "Name")]
89    pub name: String,
90    #[serde(rename = "RenditionName")]
91    pub rendition_name: String,
92    #[serde(rename = "Width")]
93    pub width: u32,
94    #[serde(rename = "Height")]
95    pub height: u32,
96    #[serde(rename = "Scale")]
97    pub scale: u32,
98    #[serde(rename = "Layout")]
99    pub layout: &'static str,
100    #[serde(flatten)]
101    pub payload: ResolvedPayloadInfo,
102    #[serde(rename = "Idiom", skip_serializing_if = "Option::is_none", default)]
103    pub idiom: Option<AttributeValue>,
104    #[serde(
105        rename = "AttributeScale",
106        skip_serializing_if = "Option::is_none",
107        default
108    )]
109    pub attribute_scale: Option<u16>,
110    #[serde(rename = "Element", skip_serializing_if = "Option::is_none", default)]
111    pub element: Option<u16>,
112    #[serde(rename = "Part", skip_serializing_if = "Option::is_none", default)]
113    pub part: Option<u16>,
114    #[serde(rename = "State", skip_serializing_if = "Option::is_none", default)]
115    pub state: Option<u16>,
116    #[serde(rename = "Layer", skip_serializing_if = "Option::is_none", default)]
117    pub layer: Option<u16>,
118    #[serde(rename = "Value", skip_serializing_if = "Option::is_none", default)]
119    pub value: Option<u16>,
120    #[serde(rename = "Direction", skip_serializing_if = "Option::is_none", default)]
121    pub direction: Option<u16>,
122    #[serde(
123        rename = "DisplayGamut",
124        skip_serializing_if = "Option::is_none",
125        default
126    )]
127    pub display_gamut: Option<u16>,
128    #[serde(rename = "Subtype", skip_serializing_if = "Option::is_none", default)]
129    pub subtype: Option<u16>,
130    #[serde(
131        rename = "HorizontalSizeClass",
132        skip_serializing_if = "Option::is_none",
133        default
134    )]
135    pub horizontal_size_class: Option<u16>,
136    #[serde(
137        rename = "VerticalSizeClass",
138        skip_serializing_if = "Option::is_none",
139        default
140    )]
141    pub vertical_size_class: Option<u16>,
142    #[serde(
143        rename = "DeploymentTarget",
144        skip_serializing_if = "Option::is_none",
145        default
146    )]
147    pub deployment_target: Option<u16>,
148    #[serde(
149        rename = "ThemeAppearance",
150        skip_serializing_if = "Option::is_none",
151        default
152    )]
153    pub theme_appearance: Option<u16>,
154    #[serde(
155        rename = "GlyphWeight",
156        skip_serializing_if = "Option::is_none",
157        default
158    )]
159    pub glyph_weight: Option<u16>,
160    #[serde(rename = "GlyphSize", skip_serializing_if = "Option::is_none", default)]
161    pub glyph_size: Option<u16>,
162}
163
164impl Car {
165    pub fn document_info(&self) -> DocumentInfo {
166        let header = self.header();
167        let metadata = self.extended_metadata();
168
169        DocumentInfo {
170            asset_storage_version: header.storage_version,
171            core_ui_version: header.coreui_version,
172            schema_version: header.schema_version,
173            timestamp: header.storage_timestamp,
174            main_version_string: header.main_version_string.clone(),
175            version_string: header.version_string.clone(),
176            uuid: format_uuid(&header.uuid),
177            color_space: color_space_str(&header.color_space),
178            key_semantics: header.key_semantics,
179            rendition_count: header.rendition_count,
180            authoring_tool: metadata.authoring_tool.clone(),
181            deployment_platform: metadata.deployment_platform.clone(),
182            deployment_platform_version: metadata.deployment_platform_version.clone(),
183            thinning_arguments: metadata.thinning_args.clone(),
184        }
185    }
186
187    pub fn rendition_infos(&self) -> Vec<RenditionInfo> {
188        let mut infos = Vec::new();
189
190        for (facet_name, facet) in self.named_facets() {
191            let Some(items) = self.items_for_facet(facet) else {
192                continue;
193            };
194            for item in items {
195                infos.push(self.build_rendition_info(facet_name, item));
196            }
197        }
198
199        infos
200    }
201
202    fn build_rendition_info(&self, facet_name: &str, item: &CSIItem) -> RenditionInfo {
203        let payload = if matches!(item.layout(), LayoutType::InternalReference) {
204            self.resolve_internal_reference(item)
205                .map(|resolved| payload_info(resolved.source))
206                .unwrap_or_else(|| payload_info(item))
207        } else {
208            payload_info(item)
209        };
210
211        let mut info = RenditionInfo {
212            name: facet_name.to_string(),
213            rendition_name: item.name().to_string(),
214            width: item.width(),
215            height: item.height(),
216            scale: item.scale(),
217            layout: layout_type_str(&item.layout()),
218            payload,
219            idiom: None,
220            attribute_scale: None,
221            element: None,
222            part: None,
223            state: None,
224            layer: None,
225            value: None,
226            direction: None,
227            display_gamut: None,
228            subtype: None,
229            horizontal_size_class: None,
230            vertical_size_class: None,
231            deployment_target: None,
232            theme_appearance: None,
233            glyph_weight: None,
234            glyph_size: None,
235        };
236
237        for attr in item.attributes() {
238            match attr.name {
239                AttributeType::Idiom => {
240                    let idiom = idiom_attribute_value(attr.val);
241                    info.idiom = Some(idiom);
242                }
243                AttributeType::Scale => info.attribute_scale = Some(attr.val),
244                AttributeType::Element => info.element = Some(attr.val),
245                AttributeType::Part => info.part = Some(attr.val),
246                AttributeType::State => info.state = Some(attr.val),
247                AttributeType::Layer => info.layer = Some(attr.val),
248                AttributeType::Value => info.value = Some(attr.val),
249                AttributeType::Direction => info.direction = Some(attr.val),
250                AttributeType::DisplayGamut => info.display_gamut = Some(attr.val),
251                AttributeType::Subtype => info.subtype = Some(attr.val),
252                AttributeType::HorizontalSizeClass => {
253                    info.horizontal_size_class = Some(attr.val);
254                }
255                AttributeType::VerticalSizeClass => {
256                    info.vertical_size_class = Some(attr.val);
257                }
258                AttributeType::DeploymentTarget => info.deployment_target = Some(attr.val),
259                AttributeType::ThemeAppearance => info.theme_appearance = Some(attr.val),
260                AttributeType::GlyphWeight => info.glyph_weight = Some(attr.val),
261                AttributeType::GlyphSize => info.glyph_size = Some(attr.val),
262                _ => {}
263            }
264        }
265
266        info
267    }
268}
269
270fn payload_info(item: &CSIItem) -> ResolvedPayloadInfo {
271    ResolvedPayloadInfo {
272        encoding: encoding_str(&item.encoding()),
273        color_model: color_model_str(&item.color_model()),
274        opaque: item.header().flags.is_opaque,
275        vector_based: item.header().flags.is_vector_based,
276        flippable: item.header().flags.is_flippable,
277        tintable: item.header().flags.is_tintable,
278        asset_type: asset_type(item),
279        compression: item
280            .compression()
281            .map(|compression| compression_type_str(&compression)),
282    }
283}
284
285fn asset_type(item: &CSIItem) -> &'static str {
286    match item.header().rendition.as_ref() {
287        Some(Rendition::Color(_)) => "Color",
288        Some(Rendition::RawData(_)) => "RawData",
289        Some(Rendition::ThemeCBCK(_)) => "Image",
290        Some(Rendition::MultisizeImageSet(_)) => "MultisizeImageSet",
291        Some(Rendition::Unknown { .. }) => "Unknown",
292        None => "None",
293    }
294}
295
296fn idiom_attribute_value(idiom: u16) -> AttributeValue {
297    let value = match idiom {
298        0 => Some(Idiom::Universal),
299        1 => Some(Idiom::Phone),
300        2 => Some(Idiom::Pad),
301        3 => Some(Idiom::TV),
302        4 => Some(Idiom::Car),
303        5 => Some(Idiom::Watch),
304        6 => Some(Idiom::Marketing),
305        _ => None,
306    };
307
308    match value {
309        Some(idiom) => AttributeValue::String(idiom_str(&idiom).to_string()),
310        None => AttributeValue::Number(idiom),
311    }
312}
313
314fn encoding_str(enc: &Encoding) -> String {
315    match enc {
316        Encoding::None => "None".into(),
317        Encoding::ARGB => "ARGB".into(),
318        Encoding::Data => "Data".into(),
319        Encoding::GRAY => "GRAY".into(),
320        Encoding::JPEG => "JPEG".into(),
321        Encoding::PDF => "PDF".into(),
322        Encoding::WEBP => "WEBP".into(),
323        Encoding::ARGB16 => "ARGB16".into(),
324        Encoding::GA16 => "GA16".into(),
325        Encoding::GA8 => "GA8".into(),
326        Encoding::RGB5 => "RGB5".into(),
327        Encoding::SVG => "SVG".into(),
328        Encoding::HEIF => "HEIF".into(),
329        Encoding::Unknown { tag } => format!("Unknown({tag:?})"),
330    }
331}
332
333fn color_model_str(cm: &ColorModel) -> &'static str {
334    match cm {
335        ColorModel::None => "None",
336        ColorModel::RGB => "RGB",
337        ColorModel::Monochrome => "Monochrome",
338        ColorModel::RGB0 => "RGB0",
339        ColorModel::RGBP3 => "RGBP3",
340        ColorModel::Unknown { .. } => "Unknown",
341    }
342}
343
344fn color_space_str(cs: &ColorSpace) -> &'static str {
345    match cs {
346        ColorSpace::SRGB => "srgb",
347        ColorSpace::GrayGamma2_2 => "gray-gamma-2.2",
348        ColorSpace::DisplayP3 => "display-p3",
349        ColorSpace::ExtendedRangeSRGB => "extended-srgb",
350        ColorSpace::ExtendedLinearSRGB => "extended-linear-srgb",
351        ColorSpace::ExtendedGray => "extended-gray",
352        ColorSpace::SystemSRGB => "system-srgb",
353        ColorSpace::Unknown { .. } => "unknown",
354    }
355}
356
357fn layout_type_str(lt: &LayoutType) -> &'static str {
358    match lt {
359        LayoutType::Gradient => "Gradient",
360        LayoutType::Effect => "Effect",
361        LayoutType::Vector => "Vector",
362        LayoutType::OnePartFixedSize => "OnePartFixedSize",
363        LayoutType::OnePartTile => "OnePartTile",
364        LayoutType::OnePartScale => "OnePartScale",
365        LayoutType::ThreePartHorizontalTile => "ThreePartHorizontalTile",
366        LayoutType::ThreePartHorizontalScale => "ThreePartHorizontalScale",
367        LayoutType::ThreePartHorizontalUniform => "ThreePartHorizontalUniform",
368        LayoutType::ThreePartVerticalTile => "ThreePartVerticalTile",
369        LayoutType::ThreePartVerticalScale => "ThreePartVerticalScale",
370        LayoutType::ThreePartVerticalUniform => "ThreePartVerticalUniform",
371        LayoutType::NinePartTile => "NinePartTile",
372        LayoutType::NinePartScale => "NinePartScale",
373        LayoutType::NinePartHorizontalUniformVerticalScale => {
374            "NinePartHorizontalUniformVerticalScale"
375        }
376        LayoutType::NinePartHorizontalScaleVerticalUniform => {
377            "NinePartHorizontalScaleVerticalUniform"
378        }
379        LayoutType::NinePartEdgesOnly => "NinePartEdgesOnly",
380        LayoutType::SixPart => "SixPart",
381        LayoutType::AnimationFilmstrip => "AnimationFilmstrip",
382        LayoutType::Data => "Data",
383        LayoutType::ExternalLink => "ExternalLink",
384        LayoutType::LayerStack => "LayerStack",
385        LayoutType::InternalReference => "InternalReference",
386        LayoutType::PackedImage => "PackedImage",
387        LayoutType::NameList => "NameList",
388        LayoutType::UnknownAddObject => "UnknownAddObject",
389        LayoutType::Texture => "Texture",
390        LayoutType::TextureImage => "TextureImage",
391        LayoutType::Color => "Color",
392        LayoutType::MultisizeImage => "MultisizeImage",
393        LayoutType::LayerReference => "LayerReference",
394        LayoutType::ContentRendition => "ContentRendition",
395        LayoutType::RecognitionObject => "RecognitionObject",
396        LayoutType::Unknown { .. } => "Unknown",
397    }
398}
399
400fn idiom_str(idiom: &Idiom) -> &'static str {
401    match idiom {
402        Idiom::Universal => "universal",
403        Idiom::Phone => "phone",
404        Idiom::Pad => "pad",
405        Idiom::TV => "tv",
406        Idiom::Car => "car",
407        Idiom::Watch => "watch",
408        Idiom::Marketing => "marketing",
409        Idiom::Unknown { .. } => "unknown",
410    }
411}
412
413fn compression_type_str(ct: &rendition::CompressionType) -> &'static str {
414    match ct {
415        rendition::CompressionType::Uncompressed => "Uncompressed",
416        rendition::CompressionType::Rle => "RLE",
417        rendition::CompressionType::Zip => "Zip",
418        rendition::CompressionType::Lzvn => "LZVN",
419        rendition::CompressionType::Lzfse => "LZFSE",
420        rendition::CompressionType::JpegLzfse => "JPEG+LZFSE",
421        rendition::CompressionType::Blurred => "Blurred",
422        rendition::CompressionType::Astc => "ASTC",
423        rendition::CompressionType::PaletteImg => "PaletteImg",
424        rendition::CompressionType::HEVC => "HEVC",
425        rendition::CompressionType::DeepmapLzfse => "Deepmap+LZFSE",
426        rendition::CompressionType::Deepmap2 => "Deepmap2",
427        rendition::CompressionType::Unknown { .. } => "Unknown",
428    }
429}
430
431fn format_uuid(uuid: &[u8; 16]) -> String {
432    format!(
433        "{:02X}{:02X}{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
434        uuid[0],
435        uuid[1],
436        uuid[2],
437        uuid[3],
438        uuid[4],
439        uuid[5],
440        uuid[6],
441        uuid[7],
442        uuid[8],
443        uuid[9],
444        uuid[10],
445        uuid[11],
446        uuid[12],
447        uuid[13],
448        uuid[14],
449        uuid[15],
450    )
451}