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 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 }
40
41pub use sb::skresources_ImageDecodeStrategy as ImageDecodeStrategy;
42variant_name!(ImageDecodeStrategy::LazyDecode);
43
44pub 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 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(¶m) })
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#[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
195impl 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)]
204pub 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
247pub mod helpers {
249 use super::ResourceProvider;
250 use crate::{Data, FontMgr, FontStyle, Typeface};
251
252 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 font_mgr.legacy_make_typeface(None, FontStyle::default())
264 }
265
266 #[derive(Debug)]
267 pub enum ResourceKind {
268 Base64(Data),
270 DownloadFromUrl(String),
272 }
273
274 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 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 let spaces_removed = remove_html_space_characters(data[1]);
298 let percent_decoded =
300 percent_encoding::percent_decode_str(&spaces_removed).decode_utf8_lossy();
301 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 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 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 assert_eq!(0, decode_base64("SGVsbG8hIQ===").len());
357
358 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 assert_eq!(0, decode_base64("$GVsbG8h").len());
365 }
366}