diff --git a/apps/app/src/api/minecraft_skins.rs b/apps/app/src/api/minecraft_skins.rs index 9daf52e84..d8e53caa4 100644 --- a/apps/app/src/api/minecraft_skins.rs +++ b/apps/app/src/api/minecraft_skins.rs @@ -1,6 +1,8 @@ use crate::api::Result; -use theseus::minecraft_skins::{self, Bytes, Cape, MinecraftSkinVariant, Skin}; +use theseus::minecraft_skins::{ + self, Bytes, Cape, MinecraftSkinVariant, Skin, UrlOrBlob, +}; pub fn init() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new("minecraft-skins") @@ -86,6 +88,6 @@ pub async fn unequip_skin() -> Result<()> { /// /// See also: [minecraft_skins::normalize_skin_texture] #[tauri::command] -pub async fn normalize_skin_texture(skin: Skin) -> Result { - Ok(minecraft_skins::normalize_skin_texture(&skin).await?) +pub async fn normalize_skin_texture(texture: UrlOrBlob) -> Result { + Ok(minecraft_skins::normalize_skin_texture(&texture).await?) } diff --git a/packages/app-lib/src/api/minecraft_skins.rs b/packages/app-lib/src/api/minecraft_skins.rs index 08f0341c5..8c9a8697a 100644 --- a/packages/app-lib/src/api/minecraft_skins.rs +++ b/packages/app-lib/src/api/minecraft_skins.rs @@ -6,8 +6,7 @@ use std::sync::{ }; pub use bytes::Bytes; -use data_url::DataUrl; -use futures::{Stream, StreamExt, TryStreamExt, future::Either, stream}; +use futures::{StreamExt, TryStreamExt, stream}; use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; @@ -21,7 +20,6 @@ use crate::{ CustomMinecraftSkin, DefaultMinecraftCape, mojang_api, }, }, - util::fetch::REQWEST_CLIENT, }; use super::data::Credentials; @@ -74,32 +72,6 @@ pub struct Skin { pub is_equipped: bool, } -impl Skin { - /// Resolves the skin texture URL to a stream of bytes. - pub async fn resolve_texture( - &self, - ) -> crate::Result> + use<>> - { - if self.texture.scheme() == "data" { - let data = DataUrl::process(self.texture.as_str())? - .decode_to_vec()? - .0 - .into(); - - Ok(Either::Left(stream::once(async { Ok(data) }))) - } else { - let response = REQWEST_CLIENT - .get(self.texture.as_str()) - .header("Accept", "image/png") - .send() - .await - .and_then(|response| response.error_for_status())?; - - Ok(Either::Right(response.bytes_stream())) - } - } -} - #[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum SkinSource { @@ -111,6 +83,14 @@ pub enum SkinSource { Custom, } +/// Represents either a URL or a blob for a Minecraft skin PNG texture. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum UrlOrBlob { + Url(Url), + Blob(Bytes), +} + /// Retrieves the available capes for the currently selected Minecraft profile. At most one cape /// can be equipped at a time. Also, at most one cape can be set as the default cape. #[tracing::instrument] @@ -398,7 +378,7 @@ pub async fn equip_skin(skin: Skin) -> crate::Result<()> { mojang_api::MinecraftSkinOperation::equip( &selected_credentials, - skin.resolve_texture().await?, + png_util::url_to_data_stream(&skin.texture).await?, skin.variant, ) .await?; @@ -467,10 +447,12 @@ pub async fn unequip_skin() -> crate::Result<()> { /// PNG encoding speed over compression density, so the resulting textures are better /// suited for display purposes, not persistent storage or transmission. /// -/// Returns the normalized, processed texture as a byte array in PNG format. +/// The normalized, processed is returned texture as a byte array in PNG format. #[tracing::instrument] -pub async fn normalize_skin_texture(skin: &Skin) -> crate::Result { - png_util::normalize_skin_texture(skin).await +pub async fn normalize_skin_texture( + texture: &UrlOrBlob, +) -> crate::Result { + png_util::normalize_skin_texture(texture).await } /// Synchronizes the equipped cape with the selected cape if necessary, taking into @@ -526,8 +508,7 @@ async fn save_current_custom_external_skin( CustomMinecraftSkin::add( selected_credentials.offline_profile.id, ¤t_external_skin.texture_key, - ¤t_external_skin - .resolve_texture() + &png_util::url_to_data_stream(¤t_external_skin.texture) .await? .try_fold(vec![], async |mut texture_blob, chunk| { texture_blob.extend_from_slice(&chunk); diff --git a/packages/app-lib/src/api/minecraft_skins/png_util.rs b/packages/app-lib/src/api/minecraft_skins/png_util.rs index 3129e64f2..821b13a80 100644 --- a/packages/app-lib/src/api/minecraft_skins/png_util.rs +++ b/packages/app-lib/src/api/minecraft_skins/png_util.rs @@ -5,13 +5,33 @@ use std::sync::Arc; use base64::Engine; use bytemuck::{AnyBitPattern, NoUninit}; use bytes::Bytes; -use futures::TryStreamExt; +use data_url::DataUrl; +use futures::{Stream, TryStreamExt, future::Either, stream}; use tokio_util::{compat::FuturesAsyncReadCompatExt, io::SyncIoBridge}; use url::Url; -use crate::ErrorKind; +use crate::{ + ErrorKind, minecraft_skins::UrlOrBlob, util::fetch::REQWEST_CLIENT, +}; -use super::Skin; +pub async fn url_to_data_stream( + url: &Url, +) -> crate::Result> + use<>> { + if url.scheme() == "data" { + let data = DataUrl::process(url.as_str())?.decode_to_vec()?.0.into(); + + Ok(Either::Left(stream::once(async { Ok(data) }))) + } else { + let response = REQWEST_CLIENT + .get(url.as_str()) + .header("Accept", "image/png") + .send() + .await + .and_then(|response| response.error_for_status())?; + + Ok(Either::Right(response.bytes_stream())) + } +} pub fn blob_to_data_url(png_data: impl AsRef<[u8]>) -> Option> { let png_data = png_data.as_ref(); @@ -69,14 +89,27 @@ pub fn dimensions(png_data: &[u8]) -> crate::Result<(u32, u32)> { /// PNG encoding speed over compression density, so the resulting textures are better /// suited for display purposes, not persistent storage or transmission. /// -/// Returns the normalized, processed texture as a byte array in PNG format. -pub async fn normalize_skin_texture(skin: &Skin) -> crate::Result { +/// The normalized, processed is returned texture as a byte array in PNG format. +pub async fn normalize_skin_texture( + texture: &UrlOrBlob, +) -> crate::Result { let texture_stream = SyncIoBridge::new(Box::pin( - skin.resolve_texture() - .await? - .map_err(std::io::Error::other) - .into_async_read() - .compat(), + match texture { + UrlOrBlob::Url(url) => Either::Left( + url_to_data_stream(url) + .await? + .map_err(std::io::Error::other) + .into_async_read(), + ), + UrlOrBlob::Blob(blob) => Either::Right( + stream::once({ + let blob = Bytes::clone(blob); + async { Ok(blob) } + }) + .into_async_read(), + ), + } + .compat(), )); tokio::task::spawn_blocking(|| { @@ -253,23 +286,14 @@ fn copy_rect_mirror_horizontally( #[cfg(test)] #[tokio::test] async fn normalize_skin_texture_works() { - use crate::{minecraft_skins::SkinSource, state::MinecraftSkinVariant}; - let legacy_png_data = &include_bytes!("assets/default/MissingNo.png")[..]; let expected_normalized_png_data = &include_bytes!("assets/test/MissingNo_normalized.png")[..]; - let normalized_png_data = normalize_skin_texture(&Skin { - texture_key: "missingno".into(), - name: None, - variant: MinecraftSkinVariant::Classic, - cape_id: None, - texture: blob_to_data_url(legacy_png_data).unwrap(), - source: SkinSource::Default, - is_equipped: false, - }) - .await - .expect("Failed to normalize skin texture"); + let normalized_png_data = + normalize_skin_texture(&UrlOrBlob::Blob(legacy_png_data.into())) + .await + .expect("Failed to normalize skin texture"); let decode_to_pixels = |png_data: &[u8]| { let decoder = png::Decoder::new(png_data);