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