From ff50964f251adf9ef2005d020a503d56555c672f Mon Sep 17 00:00:00 2001 From: Jerozgen Date: Fri, 19 Sep 2025 00:35:19 +0300 Subject: [PATCH] Strip alpha from inner skin parts (#4373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Strip alpha from inner skin parts * Notch transparency hack * Apply suggestions from code review Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com> Signed-off-by: Jerozgen * Enable `extern_crate_alloc` feature for `bytemuck` --------- Signed-off-by: Jerozgen Co-authored-by: Alejandro González <7822554+AlexTMjugador@users.noreply.github.com> --- packages/app-lib/Cargo.toml | 2 +- .../assets/test/MissingNo_normalized.png | Bin 7155 -> 0 bytes .../minecraft_skins/assets/test/legacy.png | Bin 0 -> 435 bytes .../assets/test/legacy_normalized.png | Bin 0 -> 1885 bytes .../api/minecraft_skins/assets/test/notch.png | Bin 0 -> 409 bytes .../assets/test/notch_normalized.png | Bin 0 -> 1182 bytes .../assets/test/transparent.png | Bin 0 -> 934 bytes .../assets/test/transparent_normalized.png | Bin 0 -> 1784 bytes .../src/api/minecraft_skins/png_util.rs | 271 +++++++++++++----- 9 files changed, 193 insertions(+), 80 deletions(-) delete mode 100644 packages/app-lib/src/api/minecraft_skins/assets/test/MissingNo_normalized.png create mode 100644 packages/app-lib/src/api/minecraft_skins/assets/test/legacy.png create mode 100644 packages/app-lib/src/api/minecraft_skins/assets/test/legacy_normalized.png create mode 100644 packages/app-lib/src/api/minecraft_skins/assets/test/notch.png create mode 100644 packages/app-lib/src/api/minecraft_skins/assets/test/notch_normalized.png create mode 100644 packages/app-lib/src/api/minecraft_skins/assets/test/transparent.png create mode 100644 packages/app-lib/src/api/minecraft_skins/assets/test/transparent_normalized.png diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index 659a9d9c..c6af3741 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -31,7 +31,7 @@ enumset.workspace = true chardetng.workspace = true encoding_rs.workspace = true png.workspace = true -bytemuck.workspace = true +bytemuck = { workspace = true, features = ["extern_crate_alloc"] } rgb.workspace = true phf.workspace = true itertools.workspace = true diff --git a/packages/app-lib/src/api/minecraft_skins/assets/test/MissingNo_normalized.png b/packages/app-lib/src/api/minecraft_skins/assets/test/MissingNo_normalized.png deleted file mode 100644 index 639b3fe15949a68ebfc83561109b4d764e5b4063..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7155 zcma)BYlv1=6de&Y6#YAp#DG+PB!r3HpcRUlWKS|JW`vZmAtb@f$MBI75fwu(Ljord zvWGc^sHlzRpU@si;G-;^5po1&MspZvzG?iv_TKlb{kY#94SdW!_nfoWUTf`r&egY{ zSTJ|;#G5D9)zwX&_sHz0=b`liAA08XF}**G{p{97n|}PK zy1MVZ!@q1>Ise5Sul%_?{aXI+mQ9a#Y-zl6T3`9KJuk2Q{H+ICj<=PLT z_pLj3{PgsWzNR&+27mf`|H7uzomc49tczO@9BChTvg_}o!(Xh3BQ(E%*3^;t&yK#F zb{<*OFz{Gqd)x5sr4w^oKMW%jZgcgI?&d3TPA=$6t;g;u@0@wzQwAw8ibojv$kFlD z4~i1890i7jpvg8uiT9N59{OZ^o`<64G+FYZECRg7>dr$4mj)Vbd4BR0&LwefES1|Q zS8fy90S~;fE^di`td$9D1m^vc{j&z<_-BL!N)eA3`Tg3LGg$o9r3>%K*nxAWS~^LN z=XZ3T*z>}=Z;a07q>Td%X31Cg|U%n=_a3o73&X(@Yl) z!2%tuXL$y<9XMUt_Ow`oETD46&^_A5$(V z&Abp+&=edQCa?c_*U7b+S8SXkOY+P&&lqO=8<&hq2w+~ym&Q?~Gpvx-g-m6V!EI4_ zgEOqIE%&rK=58>98~zU+j3}>+unQJApTwZ~Xj4na0I;&Y27spel{aKUTpG8yu5pFb zQE6fOuuZ90xPZl8>A6gp@2Ylovuvtgu$#y?nnQd^M!1x5R0EZiIV|f#zK~if>kj4Z zj*J}JsACkU7?<~PMi%Giq3Lt1M;DTJ7#ZAf{;D>4PngOq-Fp(BB?>><*<=^Zh&vfU zt5S9;bh&;#pEW4p)Lt1Xz2wpUjNHmVXE%p5&O1092!P`nY(Y~ zBWSIPC%>YWmIfQ)cQ7D9wR)9x@H5bCf0>otuM?{Uy6-sgB z)4@j#n3rWl&sN=8A-I0e-Htr~lC3^4((~rVOi-&aJDitcNW*0ygw#@M zyoW1OCyNXk&AJ##ZDVMRNW=sI{mxXm&*`Ntv`LS1F5eMvB@_!jd}S$cj(Ye2H8 zTFmXaTE?T70l6zSZG5nywpk~vnlC4Y8C$fWN7tRm4Z(xD32<>9h$^^rMkwiIParma ztprIYZhfsqA9p@lI=5vDSdU<;sSEQ+*VUXZ+1+lp-jcEja~a7eO2-1OZNyr_WmAmUQRH#$dV zOr9KPIT%V>8keKB8>{ELtM}db z$F|+s9WfZi_z;XE`oYC~3D87bVxinNuKJt5iOZQCjtl9ar9*hZl;XRYT*IkQX8jG5 zDqH1SN8&;UNtdJc!dOo@6NJliJdNx{Sg?8up4_~TF~a`6PVM9dm6Iv-@hJ%)`97E6(`=GVfF zKcd`yK|lQ^1%EF>fS#@il}_9Wlv{bLg$>q~m1OqN&;oKH3BFC$q2OUxT48P#9OikR zJbL4_Qnx_bb8IN<#|f;ro;Cr2sI&-^N#doI)>Jyyy!6QCSg=cMYcPWKAIyVPVk^TW ze#J@S8BfZW2j3`lsA*)j;*BZ$lYo-V;wa>xo$aRvQFypwN+{2W-rxlii6`>o?j&0l z0!oQMb(xqcQ*@(1710bmWAz8mB~1Vj&6Y|e;(&8OFdBtA*C1dM8&Ushd*eBru-gv2 zNsn8Ma?E`B?x5$SgdZX;8yt$IL_QN5()vv)kaWW#^q|M&g? diff --git a/packages/app-lib/src/api/minecraft_skins/assets/test/legacy.png b/packages/app-lib/src/api/minecraft_skins/assets/test/legacy.png new file mode 100644 index 0000000000000000000000000000000000000000..54d69181d88c040d569fab593a962fedfa1e183a GIT binary patch literal 435 zcmV;k0ZjghP)2HGIUkZ?5 zq7;WLfl_p-cGC9=BvDb20%Vz`6Y~V$!dXxT(zXmar=uY2ngAs-(*--GJr?f;k*C1q zI*D~6Oi6a2CTQKqbh0gWTWG?PIj{}^l&^_U>-JutD1r*C0ZO38^k_VLR?lT7^!Oc5|o^c%(-JgQ?rJ1 daK`Ob1Xsq&BC?6uATAhg zDza1tLj?rH8aAz3rx96h3zVcq1w{u7Dy>LoFeu{EVgA5;IPduZ?{l7WG9gZEXS>Q4 z006s~=*Yz77W^;Pgk`qw&wK&^ME96T;kLXl(qu5aI3g#0$or&H(DwaVP2HZDtV4k* z)foFLwz?}QkPN}8%%}2_im|>X1~@2MPqrZ#6W&7gXnhC?dT~@Xhzu(4)NW`_BUG6A=4;-zT z`OsN_j&ced>63@#&vsQad0KVL#-HCpJY-xy3Iwu2Z*HtWL89+v!bYnsJ}TrMHKsB= z38$nfht&DZCdQ2zvOVrcP9b}oo${N7LRIaTqQSsewA-XEy#j<%@ct$TI&-&QOx|t()Rf*p02YlXqG}5OxSs z_t#>1+}&0&vV+FDaWgWQK2EM4=N1xgl3^g^>UHNo6sP>WR?Jni-6m{T|lWEoc$)Uhv8~;~t+Ms{@<-yG~#vHHs^G z5hVe6%w5~dIHm7sG}QwW{emyER0sUs2ynvpWorRnbJP)U8+Qw+&R}e&j7Ox-V(+h9dd%Q6(xQ+ltQzk6Vum7UOOf2y8Dm=Wfu!RDrcRG=3wD9cKH4F zXYtcTnp#kLKsb+RslEJ^_6cn|(MxDYL&pEwgh zq^pw4CaMI~NaPDynKKK;sAn; zLjp=jg7ZcSEhl)0Eeq#r6upA{RofcxVCfTOgxt%zLIlb`aRW>KF4ei=hs;z@T!c~S zR{_6cf+UnD&tOLX`N4tCM9y*|%BN1Rkic*({^SZ;ymA7Y%31bXXyPo7b5SwcFFy23 z&(~KCo@<(4Y0IR;$+R<9e0EWtqHs0TUx}ThWXk!lf*^L2^X=WrjDVXp+-yf{@{c^% zy%-7XI)MEaOKGC+a!!&+W1r0E6tz3x=k=EWleL3)Xiu+se|3Zfw8|#dTN$yjfojmL zii%RiF5?Q1_?&X6$_3T-_eTXRm>=VVh_}zjwQ0=5q8JQx+U#WOn;~eIPh1ek`4~X? zAZCU~@SPO!POa7>!}31wf?{F|Mrx_h)L~D=D4^rq`a(kZbJ)jf{e5WxeDM}Ep!eE} z;1H+sStvoE!eXCGb4F4JbD?h3&e?))poC=<8_)GP3QLGX7?cxgc$%40u39uq6Tfj? z-JzM39$~U7+oXFSuk11Fz-aQZ2Ak&;iLr-m*GV1Ug6`p*|CjcDk9+q~lxrI$g85ee zAEn2v6Vmc+e=1hFm_FiBoDZr3)&0LqUy>lvq#bz%=OQ75c+cO}KUxFW`kNLx`%n_! zUj5~{hj2`t_aJO@=3Wquc}ZM{lMk3ygPd&F-heN)g- zGCH=W2oB;KB$4HxXYjOtZ?fV0S)ISKAuhdwK)Lr5Fh&bOg)?`5LDSx#q9>?m9OY5A zsv=UfbIFONKV;&;dstw1qPS8uyaa_>yO*=M=-GCIKu||#ZcD}ZS$Dt^n=X$N3`x27AXF z6;T0w<`)*B4X2u+$?6xc{7aG6&S;wyZKwJb`7Ephl5_`izkb?Px_p~J N%*MFLZ#Tfo{{rt34153p literal 0 HcmV?d00001 diff --git a/packages/app-lib/src/api/minecraft_skins/assets/test/notch.png b/packages/app-lib/src/api/minecraft_skins/assets/test/notch.png new file mode 100644 index 0000000000000000000000000000000000000000..d04b391245207acf400fca6d0149480684dc2af7 GIT binary patch literal 409 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 crate::Result<(u32, u32)> { Ok((width, height)) } -/// Normalizes the texture of a Minecraft skin to the modern 64x64 format, handling -/// legacy 64x32 skins as the vanilla game client does. This function prioritizes -/// PNG encoding speed over compression density, so the resulting textures are better -/// suited for display purposes, not persistent storage or transmission. +/// Normalizes the texture of a Minecraft skin to the modern 64x64 format, handling legacy 64x32 +/// skins, doing "Notch transparency hack" and making inner parts opaque as the vanilla game client +/// does. This function prioritizes PNG encoding speed over compression density, so the resulting +/// textures are better suited for display purposes, not persistent storage or transmission. /// /// The normalized, processed is returned texture as a byte array in PNG format. pub async fn normalize_skin_texture( @@ -131,43 +133,30 @@ pub async fn normalize_skin_texture( } let is_legacy_skin = png_reader.info().height == 32; - - let mut texture_buf = if is_legacy_skin { - // Legacy skins have half the height, so duplicate the rows to - // turn them into a 64x64 texture - vec![0; png_reader.output_buffer_size() * 2] - } else { - // Modern skins are left as-is - vec![0; png_reader.output_buffer_size()] - }; - - let texture_buf_color_type = png_reader.output_color_type().0; - png_reader.next_frame(&mut texture_buf)?; - + let mut texture_buf = + get_skin_texture_buffer(&mut png_reader, is_legacy_skin)?; if is_legacy_skin { - convert_legacy_skin_texture( - &mut texture_buf, - texture_buf_color_type, - png_reader.info(), - )?; + convert_legacy_skin_texture(&mut texture_buf, png_reader.info()); + do_notch_transparency_hack(&mut texture_buf, png_reader.info()); } + make_inner_parts_opaque(&mut texture_buf, png_reader.info()); let mut encoded_png = vec![]; let mut png_encoder = png::Encoder::new(&mut encoded_png, 64, 64); - png_encoder.set_color(texture_buf_color_type); + png_encoder.set_color(png::ColorType::Rgba); png_encoder.set_depth(png::BitDepth::Eight); png_encoder.set_filter(png::FilterType::NoFilter); png_encoder.set_compression(png::Compression::Fast); // Keeping color space information properly set, to handle the occasional - // strange PNG with non-sRGB chromacities and/or different grayscale spaces + // strange PNG with non-sRGB chromaticities and/or different grayscale spaces // that keeps most people wondering, is what sets a carefully crafted image // manipulation routine apart :) - if let Some(source_chromacities) = + if let Some(source_chromaticities) = png_reader.info().source_chromaticities.as_ref().copied() { - png_encoder.set_source_chromaticities(source_chromacities); + png_encoder.set_source_chromaticities(source_chromaticities); } if let Some(source_gamma) = png_reader.info().source_gamma.as_ref().copied() @@ -178,8 +167,10 @@ pub async fn normalize_skin_texture( png_encoder.set_source_srgb(source_srgb); } + let png_buf = bytemuck::try_cast_slice(&texture_buf) + .map_err(|_| ErrorKind::InvalidPng)?; let mut png_writer = png_encoder.write_header()?; - png_writer.write_image_data(&texture_buf)?; + png_writer.write_image_data(png_buf)?; png_writer.finish()?; Ok(encoded_png.into()) @@ -187,16 +178,71 @@ pub async fn normalize_skin_texture( .await? } +/// Reads a skin texture and returns a 64x64 buffer in RGBA format. +fn get_skin_texture_buffer( + png_reader: &mut png::Reader, + is_legacy_skin: bool, +) -> crate::Result>> { + let mut png_buf = if is_legacy_skin { + // Legacy skins have half the height, so duplicate the rows to + // turn them into a 64x64 texture + vec![0; png_reader.output_buffer_size() * 2] + } else { + // Modern skins are left as-is + vec![0; png_reader.output_buffer_size()] + }; + png_reader.next_frame(&mut png_buf)?; + + let mut texture_buf = match png_reader.output_color_type().0 { + png::ColorType::Grayscale => png_buf + .iter() + .map(|&value| Rgba { + r: value, + g: value, + b: value, + a: 255, + }) + .collect_vec(), + png::ColorType::GrayscaleAlpha => png_buf + .chunks_exact(2) + .map(|chunk| Rgba { + r: chunk[0], + g: chunk[0], + b: chunk[0], + a: chunk[1], + }) + .collect_vec(), + png::ColorType::Rgb => png_buf + .chunks_exact(3) + .map(|chunk| Rgba { + r: chunk[0], + g: chunk[1], + b: chunk[2], + a: 255, + }) + .collect_vec(), + png::ColorType::Rgba => bytemuck::try_cast_vec(png_buf) + .map_err(|_| ErrorKind::InvalidPng)?, + _ => Err(ErrorKind::InvalidPng)?, // Cannot happen by PNG spec after transformations + }; + + // Make the added bottom half of the expanded legacy skin buffer transparent + if is_legacy_skin { + set_alpha(&mut texture_buf, png_reader.info(), 0, 32, 64, 64, 0); + } + + Ok(texture_buf) +} + /// Converts a legacy skin texture (32x64 pixels) within a 64x64 buffer to the /// native 64x64 format used by modern Minecraft clients. /// /// See also 25w16a's `SkinTextureDownloader#processLegacySkin` method. #[inline] fn convert_legacy_skin_texture( - texture_buf: &mut [u8], - texture_color_type: png::ColorType, + texture_buf: &mut [Rgba], texture_info: &png::Info, -) -> crate::Result<()> { +) { /// The skin faces the game client copies around, in order, when converting a /// legacy skin to the native 64x64 format. const FACE_COPY_PARAMETERS: &[( @@ -222,33 +268,55 @@ fn convert_legacy_skin_texture( ]; for (x, y, off_x, off_y, width, height) in FACE_COPY_PARAMETERS { - macro_rules! do_copy { - ($pixel_type:ty) => { - copy_rect_mirror_horizontally::<$pixel_type>( - // This cast should never fail because all pixels have a depth of 8 bits - // after the transformations applied during decoding - ::bytemuck::try_cast_slice_mut(texture_buf).map_err(|_| ErrorKind::InvalidPng)?, - &texture_info, - *x, - *y, - *off_x, - *off_y, - *width, - *height, - ) - }; - } + copy_rect_mirror_horizontally( + texture_buf, + texture_info, + *x, + *y, + *off_x, + *off_y, + *width, + *height, + ) + } +} - match texture_color_type.samples() { - 1 => do_copy!(rgb::Gray), - 2 => do_copy!(rgb::GrayAlpha), - 3 => do_copy!(rgb::Rgb), - 4 => do_copy!(rgb::Rgba), - _ => Err(ErrorKind::InvalidPng)?, // Cannot happen by PNG spec after transformations - }; +/// Makes outer head layer transparent if every pixel has alpha greater or equal to 128. +/// +/// See also 25w16a's `SkinTextureDownloader#doNotchTransparencyHack` method. +fn do_notch_transparency_hack( + texture_buf: &mut [Rgba], + texture_info: &png::Info, +) { + // The skin part the game client makes transparent + let (x1, y1, x2, y2) = (32, 0, 64, 32); + + for y in y1..y2 { + for x in x1..x2 { + if texture_buf[x + y * texture_info.width as usize].a < 128 { + return; + } + } } - Ok(()) + set_alpha(texture_buf, texture_info, x1, y1, x2, y2, 0); +} + +/// Makes inner parts of a skin texture opaque. +/// +/// See also 25w16a's `SkinTextureDownloader#processLegacySkin` method. +#[inline] +fn make_inner_parts_opaque( + texture_buf: &mut [Rgba], + texture_info: &png::Info, +) { + /// The skin parts the game client makes opaque. + const OPAQUE_PART_PARAMETERS: &[(usize, usize, usize, usize)] = + &[(0, 0, 32, 16), (0, 16, 64, 32), (16, 48, 48, 64)]; + + for (x1, y1, x2, y2) in OPAQUE_PART_PARAMETERS { + set_alpha(texture_buf, texture_info, *x1, *y1, *x2, *y2, 255); + } } /// Copies a `width` pixels wide, `height` pixels tall rectangle of pixels within `texture_buf` @@ -260,8 +328,8 @@ fn convert_legacy_skin_texture( /// boolean, boolean)` method, but with the last two parameters fixed to `true` and `false`, /// respectively. #[allow(clippy::too_many_arguments)] -fn copy_rect_mirror_horizontally( - texture_buf: &mut [PixelType], +fn copy_rect_mirror_horizontally( + texture_buf: &mut [Rgba], texture_info: &png::Info, x: usize, y: usize, @@ -283,18 +351,27 @@ fn copy_rect_mirror_horizontally( } } +/// Sets alpha for every pixel of a rectangle within `texture_buf` +/// whose top-left corner is at `(x1, y1)` and bottom-right corner is at `(x2 - 1, y2 - 1)`. +fn set_alpha( + texture_buf: &mut [Rgba], + texture_info: &png::Info, + x1: usize, + y1: usize, + x2: usize, + y2: usize, + alpha: u8, +) { + for y in y1..y2 { + for x in x1..x2 { + texture_buf[x + y * texture_info.width as usize].a = alpha; + } + } +} + #[cfg(test)] #[tokio::test] async fn normalize_skin_texture_works() { - 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(&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); let mut reader = decoder.read_info().expect("Failed to read PNG info"); @@ -305,19 +382,55 @@ async fn normalize_skin_texture_works() { (buffer, reader.info().clone()) }; - let (normalized_pixels, normalized_info) = - decode_to_pixels(&normalized_png_data); - let (expected_pixels, expected_info) = - decode_to_pixels(expected_normalized_png_data); + let test_data = [ + ( + "legacy", + &include_bytes!("assets/test/legacy.png")[..], + &include_bytes!("assets/test/legacy_normalized.png")[..], + ), + ( + "notch", + &include_bytes!("assets/test/notch.png")[..], + &include_bytes!("assets/test/notch_normalized.png")[..], + ), + ( + "transparent", + &include_bytes!("assets/test/transparent.png")[..], + &include_bytes!("assets/test/transparent_normalized.png")[..], + ), + ]; - // Check that dimensions match - assert_eq!(normalized_info.width, expected_info.width); - assert_eq!(normalized_info.height, expected_info.height); - assert_eq!(normalized_info.color_type, expected_info.color_type); + for (skin_name, original_png_data, expected_normalized_png_data) in + test_data + { + let normalized_png_data = + normalize_skin_texture(&UrlOrBlob::Blob(original_png_data.into())) + .await + .expect("Failed to normalize skin texture"); - // Check that pixel data matches - assert_eq!( - normalized_pixels, expected_pixels, - "Pixel data doesn't match" - ); + let (normalized_pixels, normalized_info) = + decode_to_pixels(&normalized_png_data); + let (expected_pixels, expected_info) = + decode_to_pixels(expected_normalized_png_data); + + // Check that dimensions match + assert_eq!( + normalized_info.width, expected_info.width, + "Widths don't match for {skin_name}" + ); + assert_eq!( + normalized_info.height, expected_info.height, + "Heights don't match for {skin_name}" + ); + assert_eq!( + normalized_info.color_type, expected_info.color_type, + "Color types don't match for {skin_name}" + ); + + // Check that pixel data matches + assert_eq!( + normalized_pixels, expected_pixels, + "Pixel data doesn't match for {skin_name}" + ); + } }