feat(theseus): change parameter type of normalize_skin_texture Tauri command

This commit is contained in:
Alejandro González
2025-05-29 18:12:43 +02:00
parent 774950de90
commit 92f281a6df
3 changed files with 68 additions and 61 deletions

View File

@@ -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<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
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<Bytes> {
Ok(minecraft_skins::normalize_skin_texture(&skin).await?)
pub async fn normalize_skin_texture(texture: UrlOrBlob) -> Result<Bytes> {
Ok(minecraft_skins::normalize_skin_texture(&texture).await?)
}

View File

@@ -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<impl Stream<Item = Result<Bytes, reqwest::Error>> + 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<Bytes> {
png_util::normalize_skin_texture(skin).await
pub async fn normalize_skin_texture(
texture: &UrlOrBlob,
) -> crate::Result<Bytes> {
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,
&current_external_skin.texture_key,
&current_external_skin
.resolve_texture()
&png_util::url_to_data_stream(&current_external_skin.texture)
.await?
.try_fold(vec![], async |mut texture_blob, chunk| {
texture_blob.extend_from_slice(&chunk);

View File

@@ -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<impl Stream<Item = Result<Bytes, reqwest::Error>> + 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<Arc<Url>> {
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<Bytes> {
/// The normalized, processed is returned texture as a byte array in PNG format.
pub async fn normalize_skin_texture(
texture: &UrlOrBlob,
) -> crate::Result<Bytes> {
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<PixelType: NoUninit + AnyBitPattern>(
#[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);