imessage_database/message_types/
sticker.rs

1/*!
2These are [sticker messages](https://support.apple.com/guide/iphone/send-stickers-iph37b0bfe7b/ios), either from the user's sticker library or sticker apps.
3*/
4
5use std::fmt::Display;
6
7use crate::util::bundle_id::parse_balloon_bundle_id;
8
9/// Bytes for `stickerEffect:type="`
10const STICKER_EFFECT_PREFIX: [u8; 20] = [
11    115, 116, 105, 99, 107, 101, 114, 69, 102, 102, 101, 99, 116, 58, 116, 121, 112, 101, 61, 34,
12];
13/// Bytes for `"/>`
14const STICKER_EFFECT_SUFFIX: [u8; 3] = [34, 47, 62];
15
16/// Represents the source that created a sticker attachment
17#[derive(Debug, PartialEq, Eq)]
18pub enum StickerSource {
19    /// A [Genmoji](https://support.apple.com/guide/iphone/create-genmoji-with-apple-intelligence-iph4e76f5667/ios)
20    Genmoji,
21    /// A [Memoji](https://support.apple.com/en-us/111115)
22    Memoji,
23    /// User-created stickers
24    UserGenerated,
25    /// Application provided stickers
26    App(String),
27}
28
29impl StickerSource {
30    /// Given an application's bundle ID, determine the source
31    ///
32    /// # Example
33    ///
34    /// ```rust
35    /// use imessage_database::message_types::sticker::StickerSource;;
36    ///
37    /// println!("{:?}", StickerSource::from_bundle_id("com.apple.messages.genmoji")); // StickerSource::Genmoji
38    /// ```
39    pub fn from_bundle_id(bundle_id: &str) -> Option<Self> {
40        match parse_balloon_bundle_id(Some(bundle_id)) {
41            Some("com.apple.messages.genmoji") => Some(StickerSource::Genmoji),
42            Some("com.apple.Animoji.StickersApp.MessagesExtension")
43            | Some("com.apple.Jellyfish.Animoji") => Some(StickerSource::Memoji),
44            Some("com.apple.Stickers.UserGenerated.MessagesExtension") => {
45                Some(StickerSource::UserGenerated)
46            }
47            Some(other) => Some(StickerSource::App(other.to_string())),
48            None => None,
49        }
50    }
51}
52
53/// Represents different types of [sticker effects](https://www.macrumors.com/how-to/add-effects-to-stickers-in-messages/) that can be applied to sticker iMessage balloons.
54#[derive(Debug, PartialEq, Eq)]
55pub enum StickerEffect {
56    /// Sticker sent with no effect
57    Normal,
58    /// Internally referred to as `stroke`
59    Outline,
60    Comic,
61    Puffy,
62    /// Internally referred to as `iridescent`
63    Shiny,
64    Other(String),
65}
66
67impl StickerEffect {
68    /// Determine the type of a sticker from parsed `HEIC` `EXIF` data
69    fn from_exif(sticker_effect_type: &str) -> Self {
70        match sticker_effect_type {
71            "stroke" => Self::Outline,
72            "comic" => Self::Comic,
73            "puffy" => Self::Puffy,
74            "iridescent" => Self::Shiny,
75            other => Self::Other(other.to_owned()),
76        }
77    }
78}
79
80impl Display for StickerEffect {
81    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            StickerEffect::Normal => write!(fmt, "Normal"),
84            StickerEffect::Outline => write!(fmt, "Outline"),
85            StickerEffect::Comic => write!(fmt, "Comic"),
86            StickerEffect::Puffy => write!(fmt, "Puffy"),
87            StickerEffect::Shiny => write!(fmt, "Shiny"),
88            StickerEffect::Other(name) => write!(fmt, "{name}"),
89        }
90    }
91}
92
93impl Default for StickerEffect {
94    fn default() -> Self {
95        Self::Normal
96    }
97}
98
99/// Parse the sticker effect type from the EXIF data of a HEIC blob
100pub fn get_sticker_effect(mut heic_data: Vec<u8>) -> StickerEffect {
101    // Find the start index and drain
102    for idx in 0..heic_data.len() {
103        if idx + STICKER_EFFECT_PREFIX.len() < heic_data.len() {
104            let part = &heic_data[idx..idx + STICKER_EFFECT_PREFIX.len()];
105            if part == STICKER_EFFECT_PREFIX {
106                // Remove the start pattern from the blob
107                heic_data.drain(..idx + STICKER_EFFECT_PREFIX.len());
108                break;
109            }
110        } else {
111            return StickerEffect::Normal;
112        }
113    }
114
115    // Find the end index and truncate
116    for idx in 1..heic_data.len() {
117        if idx >= heic_data.len() - 2 {
118            return StickerEffect::Other("Unknown".to_string());
119        }
120        let part = &heic_data[idx..idx + STICKER_EFFECT_SUFFIX.len()];
121
122        if part == STICKER_EFFECT_SUFFIX {
123            // Remove the end pattern from the string
124            heic_data.truncate(idx);
125            break;
126        }
127    }
128    StickerEffect::from_exif(&String::from_utf8_lossy(&heic_data))
129}
130
131#[cfg(test)]
132mod tests {
133    use std::env::current_dir;
134    use std::fs::File;
135    use std::io::Read;
136
137    use crate::message_types::sticker::{get_sticker_effect, StickerEffect};
138
139    #[test]
140    fn test_parse_sticker_normal() {
141        let sticker_path = current_dir()
142            .unwrap()
143            .as_path()
144            .join("test_data/stickers/no_effect.heic");
145        let mut file = File::open(sticker_path).unwrap();
146        let mut bytes = vec![];
147        file.read_to_end(&mut bytes).unwrap();
148
149        let effect = get_sticker_effect(bytes);
150
151        assert_eq!(effect, StickerEffect::Normal);
152    }
153
154    #[test]
155    fn test_parse_sticker_outline() {
156        let sticker_path = current_dir()
157            .unwrap()
158            .as_path()
159            .join("test_data/stickers/outline.heic");
160        let mut file = File::open(sticker_path).unwrap();
161        let mut bytes = vec![];
162        file.read_to_end(&mut bytes).unwrap();
163
164        let effect = get_sticker_effect(bytes);
165
166        assert_eq!(effect, StickerEffect::Outline);
167    }
168
169    #[test]
170    fn test_parse_sticker_comic() {
171        let sticker_path = current_dir()
172            .unwrap()
173            .as_path()
174            .join("test_data/stickers/comic.heic");
175        let mut file = File::open(sticker_path).unwrap();
176        let mut bytes = vec![];
177        file.read_to_end(&mut bytes).unwrap();
178
179        let effect = get_sticker_effect(bytes);
180
181        assert_eq!(effect, StickerEffect::Comic);
182    }
183
184    #[test]
185    fn test_parse_sticker_puffy() {
186        let sticker_path = current_dir()
187            .unwrap()
188            .as_path()
189            .join("test_data/stickers/puffy.heic");
190        let mut file = File::open(sticker_path).unwrap();
191        let mut bytes = vec![];
192        file.read_to_end(&mut bytes).unwrap();
193
194        let effect = get_sticker_effect(bytes);
195
196        assert_eq!(effect, StickerEffect::Puffy);
197    }
198
199    #[test]
200    fn test_parse_sticker_shiny() {
201        let sticker_path = current_dir()
202            .unwrap()
203            .as_path()
204            .join("test_data/stickers/shiny.heic");
205        let mut file = File::open(sticker_path).unwrap();
206        let mut bytes = vec![];
207        file.read_to_end(&mut bytes).unwrap();
208
209        let effect = get_sticker_effect(bytes);
210
211        assert_eq!(effect, StickerEffect::Shiny);
212    }
213}