Skip to main content

starweaver_model/media/
types.rs

1//! Media data types.
2
3use serde::{Deserialize, Serialize};
4
5/// Detected media kind.
6#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum MediaKind {
9    /// PNG image.
10    Png,
11    /// JPEG image.
12    Jpeg,
13    /// GIF image.
14    Gif,
15    /// WebP image.
16    Webp,
17    /// MP4 video.
18    Mp4,
19    /// `WebM` video.
20    Webm,
21    /// Unknown binary payload.
22    Unknown,
23}
24
25impl MediaKind {
26    /// Return the canonical media type.
27    #[must_use]
28    pub const fn media_type(self) -> Option<&'static str> {
29        match self {
30            Self::Png => Some("image/png"),
31            Self::Jpeg => Some("image/jpeg"),
32            Self::Gif => Some("image/gif"),
33            Self::Webp => Some("image/webp"),
34            Self::Mp4 => Some("video/mp4"),
35            Self::Webm => Some("video/webm"),
36            Self::Unknown => None,
37        }
38    }
39
40    /// Return true for animated-capable media formats.
41    #[must_use]
42    pub const fn can_be_animated(self) -> bool {
43        matches!(self, Self::Gif | Self::Webp)
44    }
45
46    /// Return true for image media.
47    #[must_use]
48    pub const fn is_image(self) -> bool {
49        matches!(self, Self::Png | Self::Jpeg | Self::Gif | Self::Webp)
50    }
51
52    /// Return true for video media.
53    #[must_use]
54    pub const fn is_video(self) -> bool {
55        matches!(self, Self::Mp4 | Self::Webm)
56    }
57}
58
59/// Image dimensions parsed from a binary image header.
60#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
61pub struct ImageDimensions {
62    /// Width in pixels.
63    pub width: u32,
64    /// Height in pixels.
65    pub height: u32,
66}
67
68/// Media processing policy used by SDK preflight processors.
69#[allow(clippy::struct_excessive_bools)]
70#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
71pub struct MediaPolicy {
72    /// Optional maximum base64 encoded bytes accepted inline.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub max_inline_base64_bytes: Option<usize>,
75    /// Optional maximum width or height accepted for image input.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub max_image_dimension: Option<u32>,
78    /// Maximum image parts to retain, preserving newest media first.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub max_images: Option<usize>,
81    /// Maximum video parts to retain, preserving newest media first.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub max_videos: Option<usize>,
84    /// Whether GIF payloads are accepted inline.
85    pub allow_gif: bool,
86    /// Whether WebP payloads are accepted inline.
87    pub allow_webp: bool,
88    /// Whether image payloads are accepted.
89    pub allow_images: bool,
90    /// Whether video payloads are accepted.
91    pub allow_videos: bool,
92    /// Whether document/file payloads are accepted.
93    pub allow_documents: bool,
94}
95
96impl Default for MediaPolicy {
97    fn default() -> Self {
98        Self {
99            max_inline_base64_bytes: None,
100            max_image_dimension: None,
101            max_images: None,
102            max_videos: None,
103            allow_gif: true,
104            allow_webp: true,
105            allow_images: true,
106            allow_videos: true,
107            allow_documents: true,
108        }
109    }
110}
111
112/// Parsed data URL payload.
113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
114pub struct ParsedDataUrl {
115    /// Media type from the data URL prefix.
116    pub media_type: String,
117    /// Decoded payload bytes.
118    pub data: Vec<u8>,
119}
120
121/// Detect PNG, JPEG, GIF, `WebP`, MP4, or `WebM` from magic bytes.
122#[must_use]
123pub fn detect_media_kind(bytes: &[u8]) -> MediaKind {
124    if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
125        return MediaKind::Png;
126    }
127    if bytes.starts_with(b"\xff\xd8\xff") {
128        return MediaKind::Jpeg;
129    }
130    if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
131        return MediaKind::Gif;
132    }
133    if bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WEBP" {
134        return MediaKind::Webp;
135    }
136    if bytes.len() >= 12 && &bytes[4..8] == b"ftyp" {
137        return MediaKind::Mp4;
138    }
139    if bytes.starts_with(b"\x1a\x45\xdf\xa3") {
140        return MediaKind::Webm;
141    }
142    MediaKind::Unknown
143}