skia_safe/modules/
resources.rs

1use std::{borrow::Cow, ffi::CStr, mem, os::raw, ptr};
2
3use helpers::ResourceKind;
4use skia_bindings::{
5    self as sb, skresources_ImageAsset, RustResourceProvider, RustResourceProvider_Param, SkData,
6    SkFontMgr, SkRefCnt, SkRefCntBase, SkTypeface, TraitObject,
7};
8
9use crate::{prelude::*, Data, FontMgr, Typeface};
10
11pub type ImageAsset = RCHandle<skresources_ImageAsset>;
12require_base_type!(skresources_ImageAsset, SkRefCnt);
13
14impl NativeRefCountedBase for skresources_ImageAsset {
15    type Base = SkRefCntBase;
16}
17
18impl ImageAsset {
19    pub fn is_multi_frame(&self) -> bool {
20        unsafe { sb::C_ImageAsset_isMultiFrame(self.native_mut_force()) }
21    }
22
23    // TODO: wrap getFrameData()
24
25    pub fn from_data(
26        data: impl Into<Data>,
27        decode_strategy: impl Into<Option<ImageDecodeStrategy>>,
28    ) -> Option<Self> {
29        let decode_strategy = decode_strategy
30            .into()
31            .unwrap_or(ImageDecodeStrategy::LazyDecode);
32
33        ImageAsset::from_ptr(unsafe {
34            sb::C_MultiFrameImageAsset_Make(data.into().into_ptr(), decode_strategy)
35        })
36    }
37
38    // TODO: Wrapping Make(SkCodec) requires us to put a lifetime on the ImageAsset.
39}
40
41pub use sb::skresources_ImageDecodeStrategy as ImageDecodeStrategy;
42variant_name!(ImageDecodeStrategy::LazyDecode);
43
44// TODO: Wrap ExternalTrackAsset
45
46pub trait ResourceProvider {
47    fn load(&self, resource_path: &str, resource_name: &str) -> Option<Data>;
48
49    fn load_image_asset(
50        &self,
51        resource_path: &str,
52        resource_name: &str,
53        _resource_id: &str,
54    ) -> Option<ImageAsset> {
55        let data = self.load(resource_path, resource_name)?;
56        ImageAsset::from_data(data, None)
57    }
58
59    fn load_typeface(&self, name: &str, url: &str) -> Option<Typeface>;
60
61    /// This is used in the SVG Dom and _should_ be used for ipmlementing load_typeface().
62    fn font_mgr(&self) -> FontMgr;
63}
64
65pub type NativeResourceProvider = RCHandle<RustResourceProvider>;
66
67impl NativeRefCountedBase for RustResourceProvider {
68    type Base = SkRefCntBase;
69}
70
71impl<T: ResourceProvider + 'static> From<T> for NativeResourceProvider {
72    fn from(value: T) -> Self {
73        let b: Box<dyn ResourceProvider> = Box::new(value);
74        Self::from(b)
75    }
76}
77
78impl From<Box<dyn ResourceProvider>> for NativeResourceProvider {
79    fn from(resource_provider: Box<dyn ResourceProvider>) -> Self {
80        let param = RustResourceProvider_Param {
81            trait_: unsafe {
82                mem::transmute::<Box<dyn ResourceProvider>, TraitObject>(resource_provider)
83            },
84            drop: Some(drop),
85            load: Some(load),
86            loadImageAsset: Some(load_image_asset),
87            loadTypeface: Some(load_typeface),
88            fontMgr: Some(font_mgr),
89        };
90
91        let skia_resource_provider =
92            NativeResourceProvider::from_ptr(unsafe { sb::C_RustResourceProvider_New(&param) })
93                .unwrap();
94
95        return skia_resource_provider;
96
97        extern "C" fn drop(provider: TraitObject) {
98            mem::drop(unsafe {
99                mem::transmute::<TraitObject, Box<dyn ResourceProvider>>(provider)
100            });
101        }
102
103        extern "C" fn load(
104            provider: TraitObject,
105            resource_path: *const raw::c_char,
106            resource_name: *const raw::c_char,
107        ) -> *mut SkData {
108            unsafe {
109                provider_ref(&provider)
110                    .load(&uncstr(resource_path), &uncstr(resource_name))
111                    .map(|data| data.into_ptr())
112                    .unwrap_or(ptr::null_mut())
113            }
114        }
115
116        extern "C" fn load_image_asset(
117            provider: TraitObject,
118            resource_path: *const raw::c_char,
119            resource_name: *const raw::c_char,
120            resource_id: *const raw::c_char,
121        ) -> *mut skresources_ImageAsset {
122            unsafe {
123                provider_ref(&provider)
124                    .load_image_asset(
125                        &uncstr(resource_path),
126                        &uncstr(resource_name),
127                        &uncstr(resource_id),
128                    )
129                    .map(|image_asset| image_asset.into_ptr())
130                    .unwrap_or(ptr::null_mut())
131            }
132        }
133
134        extern "C" fn load_typeface(
135            provider: TraitObject,
136            name: *const raw::c_char,
137            url: *const raw::c_char,
138        ) -> *mut SkTypeface {
139            unsafe {
140                provider_ref(&provider)
141                    .load_typeface(&uncstr(name), &uncstr(url))
142                    .map(|typeface| typeface.into_ptr())
143                    .unwrap_or(ptr::null_mut())
144            }
145        }
146
147        extern "C" fn font_mgr(provider: TraitObject) -> *mut SkFontMgr {
148            unsafe { provider_ref(&provider).font_mgr().into_ptr() }
149        }
150
151        unsafe fn provider_ref(provider: &TraitObject) -> &dyn ResourceProvider {
152            mem::transmute(*provider)
153        }
154
155        unsafe fn uncstr(ptr: *const raw::c_char) -> Cow<'static, str> {
156            if !ptr.is_null() {
157                return CStr::from_ptr(ptr).to_string_lossy();
158            }
159            "".into()
160        }
161    }
162}
163
164/// A resource provider that loads only local / inline base64 resources.
165#[derive(Debug)]
166pub struct LocalResourceProvider {
167    font_mgr: FontMgr,
168}
169
170impl ResourceProvider for LocalResourceProvider {
171    fn load(&self, resource_path: &str, resource_name: &str) -> Option<Data> {
172        match helpers::identify_resource_kind(resource_path, resource_name) {
173            ResourceKind::Base64(data) => Some(data),
174            ResourceKind::DownloadFromUrl(_) => None,
175        }
176    }
177
178    fn load_typeface(&self, name: &str, url: &str) -> Option<Typeface> {
179        helpers::load_typeface(self, &self.font_mgr, name, url)
180    }
181
182    fn font_mgr(&self) -> FontMgr {
183        self.font_mgr.clone()
184    }
185}
186
187impl LocalResourceProvider {
188    pub fn new(font_mgr: impl Into<FontMgr>) -> Self {
189        Self {
190            font_mgr: font_mgr.into(),
191        }
192    }
193}
194
195/// Support a direct conversion from a [`FontMgr`] into a local native resource provider.
196impl From<FontMgr> for NativeResourceProvider {
197    fn from(font_mgr: FontMgr) -> Self {
198        LocalResourceProvider::new(font_mgr).into()
199    }
200}
201
202#[cfg(feature = "ureq")]
203#[derive(Debug)]
204/// A resource provider that uses ureq for downloading resources.
205pub struct UReqResourceProvider {
206    font_mgr: FontMgr,
207}
208
209#[cfg(feature = "ureq")]
210impl UReqResourceProvider {
211    pub fn new(font_mgr: impl Into<FontMgr>) -> Self {
212        Self {
213            font_mgr: font_mgr.into(),
214        }
215    }
216}
217
218#[cfg(feature = "ureq")]
219impl ResourceProvider for UReqResourceProvider {
220    fn load(&self, resource_path: &str, resource_name: &str) -> Option<Data> {
221        match helpers::identify_resource_kind(resource_path, resource_name) {
222            ResourceKind::Base64(data) => Some(data),
223            ResourceKind::DownloadFromUrl(url) => match ureq::get(&url).call() {
224                Ok(response) => {
225                    let mut reader = response.into_body().into_reader();
226                    let mut data = Vec::new();
227                    use std::io::Read;
228                    if reader.read_to_end(&mut data).is_err() {
229                        data.clear();
230                    };
231                    Some(Data::new_copy(&data))
232                }
233                Err(_) => None,
234            },
235        }
236    }
237
238    fn load_typeface(&self, name: &str, url: &str) -> Option<Typeface> {
239        helpers::load_typeface(self, &self.font_mgr, name, url)
240    }
241
242    fn font_mgr(&self) -> FontMgr {
243        self.font_mgr.clone()
244    }
245}
246
247/// Helpers that assist in implementing resource providers
248pub mod helpers {
249    use super::ResourceProvider;
250    use crate::{Data, FontMgr, FontStyle, Typeface};
251
252    /// Load a typeface via the `load()` function and generate it using a `FontMgr` instance.
253    pub fn load_typeface(
254        provider: &dyn ResourceProvider,
255        font_mgr: &FontMgr,
256        name: &str,
257        url: &str,
258    ) -> Option<Typeface> {
259        if let Some(data) = provider.load(url, name) {
260            return font_mgr.new_from_data(&data, None);
261        }
262        // Try to provide the default font if downloading fails.
263        font_mgr.legacy_make_typeface(None, FontStyle::default())
264    }
265
266    #[derive(Debug)]
267    pub enum ResourceKind {
268        /// Data is base64, return it as is.
269        Base64(Data),
270        /// Attempt to download the data from the given Url.
271        DownloadFromUrl(String),
272    }
273
274    /// Figure out the kind of data that should be loaded.
275    pub fn identify_resource_kind(resource_path: &str, resource_name: &str) -> ResourceKind {
276        const IS_WINDOWS_TARGET: bool = cfg!(target_os = "windows");
277
278        if resource_path.is_empty() && (!IS_WINDOWS_TARGET || resource_name.starts_with("data:")) {
279            return ResourceKind::Base64(load_base64(resource_name));
280        }
281
282        ResourceKind::DownloadFromUrl(if IS_WINDOWS_TARGET {
283            resource_name.to_string()
284        } else {
285            format!("{resource_path}/{resource_name}")
286        })
287    }
288
289    /// Try to parse base64 data from an data: URL. Returns empty [`Data`] if data can not be parsed.
290    fn load_base64(data: &str) -> Data {
291        let data: Vec<_> = data.split(',').collect();
292        if data.is_empty() || !data[0].ends_with(";base64") {
293            return Data::new_empty();
294        }
295
296        // remove spaces
297        let spaces_removed = remove_html_space_characters(data[1]);
298        // decode %xx
299        let percent_decoded =
300            percent_encoding::percent_decode_str(&spaces_removed).decode_utf8_lossy();
301        // decode base64
302        let result = decode_base64(&percent_decoded);
303        Data::new_copy(result.as_slice())
304    }
305
306    const HTML_SPACE_CHARACTERS: &[char] =
307        &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
308
309    // https://github.com/servo/servo/blob/1610bd2bc83cea8ff0831cf999c4fba297788f64/components/script/dom/window.rs#L575
310    fn remove_html_space_characters(value: &str) -> String {
311        fn is_html_space(c: char) -> bool {
312            HTML_SPACE_CHARACTERS.contains(&c)
313        }
314        let without_spaces = value
315            .chars()
316            .filter(|&c| !is_html_space(c))
317            .collect::<String>();
318
319        without_spaces
320    }
321
322    fn decode_base64(value: &str) -> Vec<u8> {
323        base64::decode(value).unwrap_or_default()
324    }
325
326    mod base64 {
327        use base64::{
328            alphabet,
329            engine::{self, GeneralPurposeConfig},
330            Engine,
331        };
332
333        pub fn decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
334            ENGINE.decode(input)
335        }
336
337        const ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
338            &alphabet::STANDARD,
339            GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
340        );
341    }
342
343    #[test]
344    fn decoding_base64() {
345        use std::str::from_utf8;
346
347        // padding length of 0-2 should be supported
348        assert_eq!("Hello", from_utf8(&decode_base64("SGVsbG8=")).unwrap());
349        assert_eq!("Hello!", from_utf8(&decode_base64("SGVsbG8h")).unwrap());
350        assert_eq!(
351            "Hello!!",
352            from_utf8(&decode_base64("SGVsbG8hIQ==")).unwrap()
353        );
354
355        // padding length of 3 is invalid
356        assert_eq!(0, decode_base64("SGVsbG8hIQ===").len());
357
358        // if input length divided by 4 gives a remainder of 1 after padding removal, it's invalid
359        assert_eq!(0, decode_base64("SGVsbG8hh").len());
360        assert_eq!(0, decode_base64("SGVsbG8hh=").len());
361        assert_eq!(0, decode_base64("SGVsbG8hh==").len());
362
363        // invalid characters in the input
364        assert_eq!(0, decode_base64("$GVsbG8h").len());
365    }
366}