livesplit_core/settings/image/
mod.rs

1use crate::platform::prelude::*;
2use core::{
3    ops::Deref,
4    sync::atomic::{AtomicUsize, Ordering},
5};
6use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
7
8#[cfg(test)]
9mod tests;
10
11#[cfg(all(feature = "std", feature = "image-shrinking"))]
12mod shrinking;
13
14static LAST_IMAGE_ID: AtomicUsize = AtomicUsize::new(0);
15
16/// Images can be used to store segment and game icons. Each image object comes
17/// with an ID that changes whenever the image is modified. IDs are unique
18/// across different images. There's no specific image format you need to use
19/// for the images.
20#[derive(Debug, Clone)]
21pub struct Image {
22    data: Vec<u8>,
23    id: usize,
24}
25
26/// Describes an owned representation of an image's data. It is suitable to be
27/// used in state objects. It can efficiently be serialized for various formats.
28/// For binary formats it gets serialized as its raw byte representation, while
29/// for textual formats it gets serialized as a Base64 Data URL instead.
30#[derive(Debug, Clone)]
31pub struct ImageData(pub Box<[u8]>);
32
33impl<T> From<T> for ImageData
34where
35    Box<[u8]>: From<T>,
36{
37    fn from(value: T) -> Self {
38        ImageData(value.into())
39    }
40}
41
42impl Deref for ImageData {
43    type Target = [u8];
44    fn deref(&self) -> &Self::Target {
45        &self.0
46    }
47}
48
49impl Serialize for ImageData {
50    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
51    where
52        S: Serializer,
53    {
54        if serializer.is_human_readable() {
55            if !self.0.is_empty() {
56                let mut buf = String::from("data:;base64,");
57
58                // SAFETY: We encode Base64 to the end of the string, which is
59                // always valid UTF-8. Once we've written it, we simply increase
60                // the length of the buffer by the amount of bytes written.
61                unsafe {
62                    let buf = buf.as_mut_vec();
63                    let encoded_len = base64_simd::STANDARD.encoded_length(self.0.len());
64                    buf.reserve_exact(encoded_len);
65                    let additional_len = base64_simd::STANDARD
66                        .encode(
67                            &self.0,
68                            base64_simd::Out::from_uninit_slice(buf.spare_capacity_mut()),
69                        )
70                        .len();
71                    buf.set_len(buf.len() + additional_len);
72                }
73
74                serializer.serialize_str(&buf)
75            } else {
76                serializer.serialize_str("")
77            }
78        } else {
79            serializer.serialize_bytes(&self.0)
80        }
81    }
82}
83
84impl<'de> Deserialize<'de> for ImageData {
85    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86    where
87        D: Deserializer<'de>,
88    {
89        if deserializer.is_human_readable() {
90            let data: &'de str = Deserialize::deserialize(deserializer)?;
91            if data.is_empty() {
92                Ok(ImageData(Box::new([])))
93            } else if let Some(encoded_image_data) = data.strip_prefix("data:;base64,") {
94                let image_data = base64_simd::STANDARD
95                    .decode_to_vec(encoded_image_data.as_bytes())
96                    .map_err(de::Error::custom)?;
97
98                Ok(ImageData(image_data.into_boxed_slice()))
99            } else {
100                Err(de::Error::custom("Invalid Data URL for image"))
101            }
102        } else {
103            Ok(ImageData(Deserialize::deserialize(deserializer)?))
104        }
105    }
106}
107
108impl PartialEq for Image {
109    fn eq(&self, other: &Image) -> bool {
110        self.id == other.id || self.data == other.data
111    }
112}
113
114impl Default for Image {
115    fn default() -> Image {
116        Image::new(&[])
117    }
118}
119
120impl<D: AsRef<[u8]>> From<D> for Image {
121    fn from(d: D) -> Self {
122        Image::new(d.as_ref())
123    }
124}
125
126impl Image {
127    /// Creates a new image with a unique ID with the image data provided.
128    pub fn new(data: &[u8]) -> Self {
129        let mut image = Image {
130            data: Vec::new(),
131            id: 0,
132        };
133        image.modify(data);
134        image
135    }
136
137    /// Loads an image from the file system. You need to provide a buffer used
138    /// for temporarily storing the image's data.
139    #[cfg(feature = "std")]
140    pub fn from_file<P>(path: P, buf: &mut Vec<u8>) -> std::io::Result<Image>
141    where
142        P: AsRef<std::path::Path>,
143    {
144        use std::io::Read;
145
146        let mut file = std::fs::File::open(path)?;
147        buf.clear();
148        file.read_to_end(buf)?;
149
150        Ok(Image::new(buf))
151    }
152
153    /// Accesses the unique ID for this image.
154    #[inline]
155    pub const fn id(&self) -> usize {
156        self.id
157    }
158
159    /// Accesses the image's data. If the image's data is empty, this returns an
160    /// empty slice.
161    #[inline]
162    pub fn data(&self) -> &[u8] {
163        &self.data
164    }
165
166    /// Modifies an image by replacing its image data with the new image data
167    /// provided. The image's ID changes to a new unique ID.
168    pub fn modify(&mut self, data: &[u8]) {
169        #[cfg(all(feature = "std", feature = "image-shrinking"))]
170        let data = {
171            const MAX_IMAGE_SIZE: u32 = 128;
172
173            shrinking::shrink(data, MAX_IMAGE_SIZE)
174        };
175        cfg_if::cfg_if! {
176            if #[cfg(target_has_atomic = "ptr")] {
177                self.id = LAST_IMAGE_ID.fetch_add(1, Ordering::Relaxed);
178            } else {
179                self.id = LAST_IMAGE_ID.load(Ordering::SeqCst) + 1;
180                LAST_IMAGE_ID.store(self.id, Ordering::SeqCst);
181            }
182        }
183        self.data.clear();
184        self.data.extend_from_slice(&data);
185    }
186
187    /// Checks if the image data is empty.
188    pub fn is_empty(&self) -> bool {
189        self.data.is_empty()
190    }
191}
192
193/// With a Cached Image ID you can track image changes. It starts with an
194/// uncached state and then gets updated with the images provided to it. It can
195/// be reset at any point in order to force a change to be detected.
196#[derive(Copy, Clone, Default, PartialEq, Eq)]
197pub enum CachedImageId {
198    /// The initial uncached state.
199    #[default]
200    Uncached,
201    /// The last image observed either was missing or contained no data.
202    NoImage,
203    /// The last image had actual data and the ID stored here.
204    Image(usize),
205}
206
207impl CachedImageId {
208    /// Updates the cached image ID based on the optional image provided to this
209    /// method. If a change is observed the image's data is returned. An empty
210    /// slice is returned when a transition to no image or no image data is
211    /// observed.
212    pub fn update_with<'i>(&mut self, image: Option<&'i Image>) -> Option<&'i [u8]> {
213        let new_value = image.map_or(CachedImageId::NoImage, |i| {
214            if i.is_empty() {
215                CachedImageId::NoImage
216            } else {
217                CachedImageId::Image(i.id())
218            }
219        });
220
221        if *self != new_value {
222            *self = new_value;
223            Some(image.map_or(&[], |i| &i.data))
224        } else {
225            None
226        }
227    }
228
229    /// Resets the state of the cached image ID to uncached.
230    pub fn reset(&mut self) {
231        *self = CachedImageId::Uncached;
232    }
233}