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