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    #[must_use]
40    pub fn from_bundle_id(bundle_id: &str) -> Option<Self> {
41        match parse_balloon_bundle_id(Some(bundle_id)) {
42            Some("com.apple.messages.genmoji") => Some(StickerSource::Genmoji),
43            Some(
44                "com.apple.Animoji.StickersApp.MessagesExtension" | "com.apple.Jellyfish.Animoji",
45            ) => Some(StickerSource::Memoji),
46            Some("com.apple.Stickers.UserGenerated.MessagesExtension") => {
47                Some(StickerSource::UserGenerated)
48            }
49            Some(other) => Some(StickerSource::App(other.to_string())),
50            None => None,
51        }
52    }
53}
54
55/// 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.
56#[derive(Debug, PartialEq, Eq)]
57pub enum StickerEffect {
58    /// Sticker sent with no effect
59    Normal,
60    /// Internally referred to as `stroke`
61    Outline,
62    /// Comic effect applied to the sticker
63    Comic,
64    /// Puffy effect applied to the sticker
65    Puffy,
66    /// Internally referred to as `iridescent`
67    Shiny,
68    /// Other unrecognized sticker effect
69    Other(String),
70}
71
72impl StickerEffect {
73    /// Determine the type of a sticker from parsed `HEIC` `EXIF` data
74    fn from_exif(sticker_effect_type: &str) -> Self {
75        match sticker_effect_type {
76            "stroke" => Self::Outline,
77            "comic" => Self::Comic,
78            "puffy" => Self::Puffy,
79            "iridescent" => Self::Shiny,
80            other => Self::Other(other.to_owned()),
81        }
82    }
83}
84
85impl Display for StickerEffect {
86    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self {
88            StickerEffect::Normal => write!(fmt, "Normal"),
89            StickerEffect::Outline => write!(fmt, "Outline"),
90            StickerEffect::Comic => write!(fmt, "Comic"),
91            StickerEffect::Puffy => write!(fmt, "Puffy"),
92            StickerEffect::Shiny => write!(fmt, "Shiny"),
93            StickerEffect::Other(name) => write!(fmt, "{name}"),
94        }
95    }
96}
97
98impl Default for StickerEffect {
99    fn default() -> Self {
100        Self::Normal
101    }
102}
103
104/// Parse the sticker effect type from the EXIF data of a HEIC blob
105#[must_use]
106pub fn get_sticker_effect(mut heic_data: Vec<u8>) -> StickerEffect {
107    // Find the start index and drain
108    for idx in 0..heic_data.len() {
109        if idx + STICKER_EFFECT_PREFIX.len() < heic_data.len() {
110            let part = &heic_data[idx..idx + STICKER_EFFECT_PREFIX.len()];
111            if part == STICKER_EFFECT_PREFIX {
112                // Remove the start pattern from the blob
113                heic_data.drain(..idx + STICKER_EFFECT_PREFIX.len());
114                break;
115            }
116        } else {
117            return StickerEffect::Normal;
118        }
119    }
120
121    // Find the end index and truncate
122    for idx in 1..heic_data.len() {
123        if idx >= heic_data.len() - 2 {
124            return StickerEffect::Other("Unknown".to_string());
125        }
126        let part = &heic_data[idx..idx + STICKER_EFFECT_SUFFIX.len()];
127
128        if part == STICKER_EFFECT_SUFFIX {
129            // Remove the end pattern from the string
130            heic_data.truncate(idx);
131            break;
132        }
133    }
134    StickerEffect::from_exif(&String::from_utf8_lossy(&heic_data))
135}
136
137#[cfg(test)]
138mod tests {
139    use std::env::current_dir;
140    use std::fs::File;
141    use std::io::Read;
142
143    use crate::message_types::sticker::{StickerEffect, get_sticker_effect};
144
145    #[test]
146    fn test_parse_sticker_normal() {
147        let sticker_path = current_dir()
148            .unwrap()
149            .as_path()
150            .join("test_data/stickers/no_effect.heic");
151        let mut file = File::open(sticker_path).unwrap();
152        let mut bytes = vec![];
153        file.read_to_end(&mut bytes).unwrap();
154
155        let effect = get_sticker_effect(bytes);
156
157        assert_eq!(effect, StickerEffect::Normal);
158    }
159
160    #[test]
161    fn test_parse_sticker_outline() {
162        let sticker_path = current_dir()
163            .unwrap()
164            .as_path()
165            .join("test_data/stickers/outline.heic");
166        let mut file = File::open(sticker_path).unwrap();
167        let mut bytes = vec![];
168        file.read_to_end(&mut bytes).unwrap();
169
170        let effect = get_sticker_effect(bytes);
171
172        assert_eq!(effect, StickerEffect::Outline);
173    }
174
175    #[test]
176    fn test_parse_sticker_comic() {
177        let sticker_path = current_dir()
178            .unwrap()
179            .as_path()
180            .join("test_data/stickers/comic.heic");
181        let mut file = File::open(sticker_path).unwrap();
182        let mut bytes = vec![];
183        file.read_to_end(&mut bytes).unwrap();
184
185        let effect = get_sticker_effect(bytes);
186
187        assert_eq!(effect, StickerEffect::Comic);
188    }
189
190    #[test]
191    fn test_parse_sticker_puffy() {
192        let sticker_path = current_dir()
193            .unwrap()
194            .as_path()
195            .join("test_data/stickers/puffy.heic");
196        let mut file = File::open(sticker_path).unwrap();
197        let mut bytes = vec![];
198        file.read_to_end(&mut bytes).unwrap();
199
200        let effect = get_sticker_effect(bytes);
201
202        assert_eq!(effect, StickerEffect::Puffy);
203    }
204
205    #[test]
206    fn test_parse_sticker_shiny() {
207        let sticker_path = current_dir()
208            .unwrap()
209            .as_path()
210            .join("test_data/stickers/shiny.heic");
211        let mut file = File::open(sticker_path).unwrap();
212        let mut bytes = vec![];
213        file.read_to_end(&mut bytes).unwrap();
214
215        let effect = get_sticker_effect(bytes);
216
217        assert_eq!(effect, StickerEffect::Shiny);
218    }
219}