use std::{borrow::Cow, ffi::CStr, mem, os::raw, ptr};
use helpers::ResourceKind;
use skia_bindings::{
self as sb, skresources_ImageAsset, RustResourceProvider, RustResourceProvider_Param, SkData,
SkFontMgr, SkRefCnt, SkRefCntBase, SkTypeface, TraitObject,
};
use crate::{prelude::*, Data, FontMgr, Typeface};
pub type ImageAsset = RCHandle<skresources_ImageAsset>;
require_base_type!(skresources_ImageAsset, SkRefCnt);
impl NativeRefCountedBase for skresources_ImageAsset {
type Base = SkRefCntBase;
}
impl ImageAsset {
pub fn is_multi_frame(&self) -> bool {
unsafe { sb::C_ImageAsset_isMultiFrame(self.native_mut_force()) }
}
pub fn from_data(
data: impl Into<Data>,
decode_strategy: impl Into<Option<ImageDecodeStrategy>>,
) -> Option<Self> {
let decode_strategy = decode_strategy
.into()
.unwrap_or(ImageDecodeStrategy::LazyDecode);
ImageAsset::from_ptr(unsafe {
sb::C_MultiFrameImageAsset_Make(data.into().into_ptr(), decode_strategy)
})
}
}
pub use sb::skresources_ImageDecodeStrategy as ImageDecodeStrategy;
variant_name!(ImageDecodeStrategy::LazyDecode);
pub trait ResourceProvider {
fn load(&self, resource_path: &str, resource_name: &str) -> Option<Data>;
fn load_image_asset(
&self,
resource_path: &str,
resource_name: &str,
_resource_id: &str,
) -> Option<ImageAsset> {
let data = self.load(resource_path, resource_name)?;
ImageAsset::from_data(data, None)
}
fn load_typeface(&self, name: &str, url: &str) -> Option<Typeface>;
fn font_mgr(&self) -> FontMgr;
}
pub type NativeResourceProvider = RCHandle<RustResourceProvider>;
impl NativeRefCountedBase for RustResourceProvider {
type Base = SkRefCntBase;
}
impl<T: ResourceProvider + 'static> From<T> for NativeResourceProvider {
fn from(value: T) -> Self {
let b: Box<dyn ResourceProvider> = Box::new(value);
Self::from(b)
}
}
impl From<Box<dyn ResourceProvider>> for NativeResourceProvider {
fn from(resource_provider: Box<dyn ResourceProvider>) -> Self {
let param = RustResourceProvider_Param {
trait_: unsafe {
mem::transmute::<Box<dyn ResourceProvider>, TraitObject>(resource_provider)
},
drop: Some(drop),
load: Some(load),
loadImageAsset: Some(load_image_asset),
loadTypeface: Some(load_typeface),
fontMgr: Some(font_mgr),
};
let skia_resource_provider =
NativeResourceProvider::from_ptr(unsafe { sb::C_RustResourceProvider_New(¶m) })
.unwrap();
return skia_resource_provider;
extern "C" fn drop(provider: TraitObject) {
mem::drop(unsafe {
mem::transmute::<TraitObject, Box<dyn ResourceProvider>>(provider)
});
}
extern "C" fn load(
provider: TraitObject,
resource_path: *const raw::c_char,
resource_name: *const raw::c_char,
) -> *mut SkData {
unsafe {
provider_ref(&provider)
.load(&uncstr(resource_path), &uncstr(resource_name))
.map(|data| data.into_ptr())
.unwrap_or(ptr::null_mut())
}
}
extern "C" fn load_image_asset(
provider: TraitObject,
resource_path: *const raw::c_char,
resource_name: *const raw::c_char,
resource_id: *const raw::c_char,
) -> *mut skresources_ImageAsset {
unsafe {
provider_ref(&provider)
.load_image_asset(
&uncstr(resource_path),
&uncstr(resource_name),
&uncstr(resource_id),
)
.map(|image_asset| image_asset.into_ptr())
.unwrap_or(ptr::null_mut())
}
}
extern "C" fn load_typeface(
provider: TraitObject,
name: *const raw::c_char,
url: *const raw::c_char,
) -> *mut SkTypeface {
unsafe {
provider_ref(&provider)
.load_typeface(&uncstr(name), &uncstr(url))
.map(|typeface| typeface.into_ptr())
.unwrap_or(ptr::null_mut())
}
}
extern "C" fn font_mgr(provider: TraitObject) -> *mut SkFontMgr {
unsafe { provider_ref(&provider).font_mgr().into_ptr() }
}
unsafe fn provider_ref(provider: &TraitObject) -> &dyn ResourceProvider {
mem::transmute(*provider)
}
unsafe fn uncstr(ptr: *const raw::c_char) -> Cow<'static, str> {
if !ptr.is_null() {
return CStr::from_ptr(ptr).to_string_lossy();
}
"".into()
}
}
}
#[derive(Debug)]
pub struct LocalResourceProvider {
font_mgr: FontMgr,
}
impl ResourceProvider for LocalResourceProvider {
fn load(&self, resource_path: &str, resource_name: &str) -> Option<Data> {
match helpers::identify_resource_kind(resource_path, resource_name) {
ResourceKind::Base64(data) => Some(data),
ResourceKind::DownloadFromUrl(_) => None,
}
}
fn load_typeface(&self, name: &str, url: &str) -> Option<Typeface> {
helpers::load_typeface(self, &self.font_mgr, name, url)
}
fn font_mgr(&self) -> FontMgr {
self.font_mgr.clone()
}
}
impl LocalResourceProvider {
pub fn new(font_mgr: impl Into<FontMgr>) -> Self {
Self {
font_mgr: font_mgr.into(),
}
}
}
impl From<FontMgr> for NativeResourceProvider {
fn from(font_mgr: FontMgr) -> Self {
LocalResourceProvider::new(font_mgr).into()
}
}
#[cfg(feature = "ureq")]
#[derive(Debug)]
pub struct UReqResourceProvider {
font_mgr: FontMgr,
}
#[cfg(feature = "ureq")]
impl UReqResourceProvider {
pub fn new(font_mgr: impl Into<FontMgr>) -> Self {
Self {
font_mgr: font_mgr.into(),
}
}
}
#[cfg(feature = "ureq")]
impl ResourceProvider for UReqResourceProvider {
fn load(&self, resource_path: &str, resource_name: &str) -> Option<Data> {
match helpers::identify_resource_kind(resource_path, resource_name) {
ResourceKind::Base64(data) => Some(data),
ResourceKind::DownloadFromUrl(url) => match ureq::get(&url).call() {
Ok(response) => {
let mut reader = response.into_reader();
let mut data = Vec::new();
if reader.read_to_end(&mut data).is_err() {
data.clear();
};
Some(Data::new_copy(&data))
}
Err(_) => None,
},
}
}
fn load_typeface(&self, name: &str, url: &str) -> Option<Typeface> {
helpers::load_typeface(self, &self.font_mgr, name, url)
}
fn font_mgr(&self) -> FontMgr {
self.font_mgr.clone()
}
}
pub mod helpers {
use super::ResourceProvider;
use crate::{Data, FontMgr, FontStyle, Typeface};
pub fn load_typeface(
provider: &dyn ResourceProvider,
font_mgr: &FontMgr,
name: &str,
url: &str,
) -> Option<Typeface> {
if let Some(data) = provider.load(url, name) {
return font_mgr.new_from_data(&data, None);
}
font_mgr.legacy_make_typeface(None, FontStyle::default())
}
#[derive(Debug)]
pub enum ResourceKind {
Base64(Data),
DownloadFromUrl(String),
}
pub fn identify_resource_kind(resource_path: &str, resource_name: &str) -> ResourceKind {
const IS_WINDOWS_TARGET: bool = cfg!(target_os = "windows");
if resource_path.is_empty() && (!IS_WINDOWS_TARGET || resource_name.starts_with("data:")) {
return ResourceKind::Base64(load_base64(resource_name));
}
ResourceKind::DownloadFromUrl(if IS_WINDOWS_TARGET {
resource_name.to_string()
} else {
format!("{resource_path}/{resource_name}")
})
}
fn load_base64(data: &str) -> Data {
let data: Vec<_> = data.split(',').collect();
if data.is_empty() || !data[0].ends_with(";base64") {
return Data::new_empty();
}
let spaces_removed = remove_html_space_characters(data[1]);
let percent_decoded =
percent_encoding::percent_decode_str(&spaces_removed).decode_utf8_lossy();
let result = decode_base64(&percent_decoded);
Data::new_copy(result.as_slice())
}
const HTML_SPACE_CHARACTERS: &[char] =
&['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
fn remove_html_space_characters(value: &str) -> String {
fn is_html_space(c: char) -> bool {
HTML_SPACE_CHARACTERS.iter().any(|&m| m == c)
}
let without_spaces = value
.chars()
.filter(|&c| !is_html_space(c))
.collect::<String>();
without_spaces
}
fn decode_base64(value: &str) -> Vec<u8> {
base64::decode(value).unwrap_or_default()
}
mod base64 {
use base64::{
alphabet,
engine::{self, GeneralPurposeConfig},
Engine,
};
pub fn decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
ENGINE.decode(input)
}
const ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
);
}
#[test]
fn decoding_base64() {
use std::str::from_utf8;
assert_eq!("Hello", from_utf8(&decode_base64("SGVsbG8=")).unwrap());
assert_eq!("Hello!", from_utf8(&decode_base64("SGVsbG8h")).unwrap());
assert_eq!(
"Hello!!",
from_utf8(&decode_base64("SGVsbG8hIQ==")).unwrap()
);
assert_eq!(0, decode_base64("SGVsbG8hIQ===").len());
assert_eq!(0, decode_base64("SGVsbG8hh").len());
assert_eq!(0, decode_base64("SGVsbG8hh=").len());
assert_eq!(0, decode_base64("SGVsbG8hh==").len());
assert_eq!(0, decode_base64("$GVsbG8h").len());
}
}