From 0a307e92213fd4ffae7d7bb22e55a18e3ac7e8aa Mon Sep 17 00:00:00 2001 From: printempw Date: Thu, 28 Jun 2018 21:55:33 +0800 Subject: [PATCH] Refine texture preview generation The methods in App\Services\Minecraft now accept binary image data as their first parameter. The parameters of Minecraft::generatePreviewFromSkin is reordered. Fix skin preview for Alex model textures. Add more parameters to Minecraft::generatePreviewFromCape for specifying image size. --- app/Http/Controllers/TextureController.php | 25 ++- app/Services/Minecraft.php | 168 ++++++++++++--------- tests/ServicesTest/MinecraftTest.php | 63 ++++---- 3 files changed, 139 insertions(+), 117 deletions(-) diff --git a/app/Http/Controllers/TextureController.php b/app/Http/Controllers/TextureController.php index 92981ceb..c6c0216e 100644 --- a/app/Http/Controllers/TextureController.php +++ b/app/Http/Controllers/TextureController.php @@ -102,9 +102,8 @@ class TextureController extends Controller if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) { return $responses[0]; // @codeCoverageIgnore } else { - $filename = config('filesystems.disks.textures.root').'/'.$t->hash; + $png = Minecraft::generateAvatarFromSkin(Storage::disk('textures')->read($t->hash), $size); - $png = Minecraft::generateAvatarFromSkin($filename, $size); ob_start(); imagepng($png); imagedestroy($png); @@ -141,24 +140,20 @@ class TextureController extends Controller if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) { return $responses[0]; // @codeCoverageIgnore } else { - $filename = config('filesystems.disks.textures.root').'/'.$t->hash; + $binary = Storage::disk('textures')->read($t->hash); if ($t->type == "cape") { - $png = Minecraft::generatePreviewFromCape($filename, $size); - ob_start(); - imagepng($png); - imagedestroy($png); - $image = ob_get_contents(); - ob_end_clean(); + $png = Minecraft::generatePreviewFromCape($binary, $size*0.8, $size*1.125, $size); } else { - $png = Minecraft::generatePreviewFromSkin($filename, $size, false, false, 4, $t->type == 'alex'); - ob_start(); - imagepng($png); - imagedestroy($png); - $image = ob_get_contents(); - ob_end_clean(); + $png = Minecraft::generatePreviewFromSkin($binary, $size, ($t->type == 'alex'), 'both', 4); } + ob_start(); + imagepng($png); + imagedestroy($png); + $image = ob_get_contents(); + ob_end_clean(); + return Response::png($image); } } diff --git a/app/Services/Minecraft.php b/app/Services/Minecraft.php index 555b927f..775b03e5 100644 --- a/app/Services/Minecraft.php +++ b/app/Services/Minecraft.php @@ -5,64 +5,72 @@ namespace App\Services; class Minecraft { /** - * Cut and resize to get avatar from skin, HD support by + * Cut and resize to get the head part from a skin image. + * HD skin support added by xfl03 . * - * @author https://github.com/jamiebicknell/Minecraft-Avatar/blob/master/face.php - * @param string $resource, img path or base64 - * @param int $size - * @param string $view, default for 'f' - * @param bool $base64, if given $resource is encoded in base64 + * @see https://github.com/jamiebicknell/Minecraft-Avatar/blob/master/face.php + * @param string $binary Binary image data or decoded base64 formatted image. + * @param int $height The height of generated image in pixel. + * @param string $view Which side of head to be captured, defaults to 'f' for front view. * @return resource */ - public static function generateAvatarFromSkin($resource, $size, $view='f', $base64 = false) + public static function generateAvatarFromSkin($binary, $height, $view = 'f') { - $src = $base64 ? imagecreatefromstring(base64_decode($resource)) : imagecreatefrompng($resource); - $dest = imagecreatetruecolor($size, $size); - $ratio = imagesx($src) / 64; // width/64 + $src = imagecreatefromstring($binary); + $dest = imagecreatetruecolor($height, $height); + $ratio = imagesx($src) / 64; - // f => front, l => left, r => right, b => back - $x = array('f' => 8, 'l' => 16, 'r' => 0, 'b' => 24); + $x = [ + 'f' => 8, // Front + 'l' => 16, // Left + 'r' => 0, // Right + 'b' => 24 // Back + ]; - imagecopyresized($dest, $src, 0, 0, $x[$view] * $ratio, 8 * $ratio, $size, $size, 8 * $ratio, 8 * $ratio); // Face - imagecolortransparent($src, imagecolorat($src, 63 * $ratio, 0)); // Black Hat Issue - imagecopyresized($dest, $src, 0, 0, ($x[$view] + 32) * $ratio, 8 * $ratio, $size, $size, 8 * $ratio, 8 * $ratio); // Accessories + imagecopyresized($dest, $src, 0, 0, $x[$view] * $ratio, 8 * $ratio, $height, $height, 8 * $ratio, 8 * $ratio); // Face + imagecolortransparent($src, imagecolorat($src, 63 * $ratio, 0)); // Black hat issue + imagecopyresized($dest, $src, 0, 0, ($x[$view] + 32) * $ratio, 8 * $ratio, $height, $height, 8 * $ratio, 8 * $ratio); // Accessories imagedestroy($src); return $dest; } /** - * Generate skin preview + * Generate a image preview for a skin texture. * - * @link https://github.com/NC22/Minecraft-HD-skin-viewer-2D/blob/master/SkinViewer2D.class.php - * @param string $resource - * @param int $size - * @param bool|string $side 'front' or 'back' - * @param bool $base64 Generate image from base64 string - * @param int $gap Gap size between front & back preview + * @see https://github.com/NC22/Minecraft-HD-skin-viewer-2D/blob/master/SkinViewer2D.class.php + * @param string $binary Binary image data or decoded base64 formatted image. + * @param int $height The height of generated image in pixel. + * @param bool $alex Whether the given skin is in Alex model. + * @param string $side Which side of model to be captured, 'front', 'back' or 'both'. + * @param int $gap Gap size between front & back preview in relative pixel. * @return resource */ - public static function generatePreviewFromSkin($resource, $size, $side = false, $base64 = false, $gap = 4, $alex = false) + public static function generatePreviewFromSkin($binary, $height, $alex = false, $side = 'both', $gap = 4) { - $src = $base64 ? imagecreatefromstring(base64_decode($resource)) : imagecreatefrompng($resource); + $src = imagecreatefromstring($binary); $ratio = imagesx($src) / 64; - /** - * Check if double layer skin given - * @var bool - */ + // Check if given skin contains double layers $double = imagesy($src) == 64 * $ratio; - $dest = imagecreatetruecolor((($side) ? 16 : 32) * $ratio + $gap * $ratio, 32 * $ratio); + $dest = imagecreatetruecolor((32 + $gap) * $ratio, 32 * $ratio); - // width of front preview + width of gap - $half_width = ($side) ? 0 : (($side) ? 8 : 16) * $ratio + $gap * $ratio; + if ($side == 'both') { + // The width of front view and gap, the back side view will be drawn on its right. + $half_width = (16 + $gap) * $ratio; + $dest = imagecreatetruecolor((32 + $gap) * $ratio, 32 * $ratio); + } else { + // No need to calculate this if only single side view is required + $half_width = 0; + $dest = imagecreatetruecolor((16 + $gap) * $ratio, 32 * $ratio); + } $transparent = imagecolorallocatealpha($dest, 255, 255, 255, 127); imagefill($dest, 0, 0, $transparent); - if (! $side or $side === 'front') { + if ($side == 'both' || $side == 'front') { imagecopy($dest, $src, 4 * $ratio, 0 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio); // Head - 1 imagecopy($dest, $src, 4 * $ratio, 0 * $ratio, 40 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio); // Head - 2 imagecopy($dest, $src, 4 * $ratio, 8 * $ratio, 20 * $ratio, 20 * $ratio, 8 * $ratio, 12 * $ratio); // Body - 1 @@ -97,20 +105,20 @@ class Minecraft } else { // I am not sure whether there are single layer Alex-model skin. if ($alex) { - self::imageflip($dest, $src, 12 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 3 * $ratio, 12 * $ratio); // Left Arm + static::imageflip($dest, $src, 12 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 3 * $ratio, 12 * $ratio); // Left Arm } else { - self::imageflip($dest, $src, 12 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Left Arm + static::imageflip($dest, $src, 12 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Left Arm } - self::imageflip($dest, $src, 8 * $ratio, 20 * $ratio, 4 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Left Leg + static::imageflip($dest, $src, 8 * $ratio, 20 * $ratio, 4 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Left Leg } } - if (! $side or $side === 'back') { - imagecopy($dest, $src, $half_width + 4 * $ratio, 8 * $ratio, 32 * $ratio, 20 * $ratio, 8 * $ratio, 12 * $ratio); // Body - imagecopy($dest, $src, $half_width + 4 * $ratio, 0 * $ratio, 24 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio); // Head - imagecopy($dest, $src, $half_width + 8 * $ratio, 20 * $ratio, 12 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Right Leg - imagecopy($dest, $src, $half_width + 4 * $ratio, 0 * $ratio, 56 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio); // Headwear + if ($side == 'both' || $side == 'back') { + imagecopy($dest, $src, $half_width + 4 * $ratio, 8 * $ratio, 32 * $ratio, 20 * $ratio, 8 * $ratio, 12 * $ratio); // Body + imagecopy($dest, $src, $half_width + 4 * $ratio, 0 * $ratio, 24 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio); // Head + imagecopy($dest, $src, $half_width + 8 * $ratio, 20 * $ratio, 12 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Right Leg + imagecopy($dest, $src, $half_width + 4 * $ratio, 0 * $ratio, 56 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio); // Headwear if ($alex) { @@ -138,18 +146,17 @@ class Minecraft imagecopy($dest, $src, $half_width + 8 * $ratio, 20 * $ratio, 12 * $ratio, 36 * $ratio, 4 * $ratio, 12 * $ratio); imagecopy($dest, $src, $half_width + 4 * $ratio, 20 * $ratio, 12 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio); } else { - self::imageflip($dest, $src, $half_width + 0 * $ratio, 8 * $ratio, 52 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); - self::imageflip($dest, $src, $half_width + 4 * $ratio, 20 * $ratio, 12 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); + static::imageflip($dest, $src, $half_width + 0 * $ratio, 8 * $ratio, 52 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); + static::imageflip($dest, $src, $half_width + 4 * $ratio, 20 * $ratio, 12 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); } } - $size_x = ($side) ? $size / 2 : $size / 32 * (32 + $gap); - $fullsize = imagecreatetruecolor($size_x, $size); + $width = ($side == 'both') ? $height / 32 * (32 + $gap) : $height / 2; + $fullsize = imagecreatetruecolor($width, $height); imagesavealpha($fullsize, true); $transparent = imagecolorallocatealpha($fullsize, 255, 255, 255, 127); imagefill($fullsize, 0, 0, $transparent); - imagecopyresized($fullsize, $dest, 0, 0, 0, 0, imagesx($fullsize), imagesy($fullsize), imagesx($dest), imagesy($dest)); imagedestroy($dest); @@ -158,37 +165,50 @@ class Minecraft return $fullsize; } - private static function imageflip(&$result, &$img, $rx = 0, $ry = 0, $x = 0, $y = 0, $size_x = null, $size_y = null) + /** + * Generate a image preview for a cape texture. + * + * @param string $binary Binary image data or decoded base64 formatted image. + * @param int $height The size of generated image in pixel. + * @param int $fillWidth Create a image with given size, And draw the preview on the center of it. + * @param int $fillHeight Set the value to 0 to disable. + * @return resource + */ + public static function generatePreviewFromCape($binary, $height, $fillWidth = 0, $fillHeight = 0) { - if ($size_x < 1) - $size_x = imagesx($img); - if ($size_y < 1) - $size_y = imagesy($img); + $src = imagecreatefromstring($binary); + $ratio = imagesx($src) / 64; + $width = $height / 16 * 10; + + $dest = imagecreatetruecolor($width, $height); + imagesavealpha($dest, true); + $transparent = imagecolorallocatealpha($dest, 255, 255, 255, 127); + imagefill($dest, 0, 0, $transparent); + imagecopyresized($dest, $src, 0, 0, $ratio, $ratio, $width, $height, imagesx($src)*10/64, imagesy($src)*16/32); + + imagedestroy($src); + if ($fillWidth == 0 || $fillHeight == 0) { + return $dest; + } + + $filled = imagecreatetruecolor($fillWidth, $fillHeight); + imagesavealpha($filled, true); + $transparent = imagecolorallocatealpha($filled, 255, 255, 255, 127); + imagefill($filled, 0, 0, $transparent); + imagecopyresized($filled, $dest, ($fillWidth-$width)/2, ($fillHeight-$height)/2, 0, 0, $width, $height, $width, $height); + + imagedestroy($dest); + return $filled; + } + + /** + * Flip the given image. + */ + protected static function imageflip(&$result, &$img, $rx = 0, $ry = 0, $x = 0, $y = 0, $size_x = null, $size_y = null) + { + $size_x = ($size_x < 1) ? $imagesx($img) : $size_x; + $size_y = ($size_y < 1) ? $imagesy($img) : $size_y; imagecopyresampled($result, $img, $rx, $ry, ($x + $size_x - 1), $y, $size_x, $size_y, 0 - $size_x, $size_y); } - - public static function generatePreviewFromCape($resource) - { - $src = imagecreatefrompng($resource); - - $dest = imagecreatetruecolor(250, 166); - imagesavealpha($dest, true); - - $trans_colour = imagecolorallocatealpha($dest, 0, 0, 0, 127); - imagefill($dest, 0, 0, $trans_colour); - - $src_width = imagesx($src) * 11 / 64; - $src_height = imagesy($src) * 17 / 32; - - $dst_height = 100; - // 100 / 17 * 11 - $dst_width = 64; - - // dst_x = (250 - 64) / 2 - imagecopyresized($dest, $src, 93, 30, 0, 0, $dst_width, $dst_height, $src_width, $src_height); - - imagedestroy($src); - return $dest; - } } diff --git a/tests/ServicesTest/MinecraftTest.php b/tests/ServicesTest/MinecraftTest.php index 47bbf811..c0742052 100644 --- a/tests/ServicesTest/MinecraftTest.php +++ b/tests/ServicesTest/MinecraftTest.php @@ -18,20 +18,17 @@ class MinecraftTest extends TestCase public function testGenerateAvatarFromSkin() { imagepng(imagecreatetruecolor(64, 32), vfsStream::url('root/skin.png')); - $avatar = Minecraft::generateAvatarFromSkin(vfsStream::url('root/skin.png'), 50); + $avatar = Minecraft::generateAvatarFromSkin(file_get_contents(vfsStream::url('root/skin.png')), 50); $this->assertEquals(50, imagesx($avatar)); $this->assertEquals(50, imagesy($avatar)); imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/skin.png')); - $avatar = Minecraft::generateAvatarFromSkin(vfsStream::url('root/skin.png'), 50); + $avatar = Minecraft::generateAvatarFromSkin(file_get_contents(vfsStream::url('root/skin.png')), 50); $this->assertEquals(50, imagesx($avatar)); $this->assertEquals(50, imagesy($avatar)); $avatar = Minecraft::generateAvatarFromSkin( - TextureController::getDefaultSteveSkin(), - 50, - 'f', - true + base64_decode(TextureController::getDefaultSteveSkin()), 50, 'l' ); $this->assertEquals(50, imagesx($avatar)); $this->assertEquals(50, imagesy($avatar)); @@ -40,49 +37,49 @@ class MinecraftTest extends TestCase public function testGeneratePreviewFromSkin() { imagepng(imagecreatetruecolor(64, 32), vfsStream::url('root/skin.png')); - $preview = Minecraft::generatePreviewFromSkin(vfsStream::url('root/skin.png'), 50, true); + $preview = Minecraft::generatePreviewFromSkin( + file_get_contents(vfsStream::url('root/skin.png')), 50, false, 'front' + ); $this->assertEquals(25, imagesx($preview)); $this->assertEquals(50, imagesy($preview)); imagepng(imagecreatetruecolor(64, 32), vfsStream::url('root/skin.png')); $preview = Minecraft::generatePreviewFromSkin( - vfsStream::url('root/skin.png'), + file_get_contents(vfsStream::url('root/skin.png')), 50, - false, - false, - 4, - true // Alex model + true, // Alex model + 'both', + 4 ); $this->assertEquals(56, imagesx($preview)); $this->assertEquals(50, imagesy($preview)); imagepng(imagecreatetruecolor(64, 64), vfsStream::url('root/skin.png')); $preview = Minecraft::generatePreviewFromSkin( - vfsStream::url('root/skin.png'), - 50, - false, - false, - 4, - true // Alex model + file_get_contents(vfsStream::url('root/skin.png')), + 100, + true, // Alex model + 'both', + 8 ); - $this->assertEquals(56, imagesx($preview)); - $this->assertEquals(50, imagesy($preview)); + $this->assertEquals(125, imagesx($preview)); + $this->assertEquals(100, imagesy($preview)); imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/skin.png')); - $preview = Minecraft::generatePreviewFromSkin(vfsStream::url('root/skin.png'), 50); + $preview = Minecraft::generatePreviewFromSkin(file_get_contents(vfsStream::url('root/skin.png')), 50); $this->assertEquals(56, imagesx($preview)); $this->assertEquals(50, imagesy($preview)); imagepng(imagecreatetruecolor(128, 128), vfsStream::url('root/skin.png')); - $preview = Minecraft::generatePreviewFromSkin(vfsStream::url('root/skin.png'), 50); + $preview = Minecraft::generatePreviewFromSkin(file_get_contents(vfsStream::url('root/skin.png')), 50); $this->assertEquals(56, imagesx($preview)); $this->assertEquals(50, imagesy($preview)); $preview = Minecraft::generatePreviewFromSkin( - TextureController::getDefaultSteveSkin(), + base64_decode(TextureController::getDefaultSteveSkin()), 50, - true, - true + false, + 'back' ); $this->assertEquals(25, imagesx($preview)); $this->assertEquals(50, imagesy($preview)); @@ -91,8 +88,18 @@ class MinecraftTest extends TestCase public function testGeneratePreviewFromCape() { imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/cape.png')); - $preview = Minecraft::generatePreviewFromCape(vfsStream::url('root/cape.png')); - $this->assertEquals(250, imagesx($preview)); - $this->assertEquals(166, imagesy($preview)); + $preview = Minecraft::generatePreviewFromCape(file_get_contents(vfsStream::url('root/cape.png')), 64); + $this->assertEquals(40, imagesx($preview)); + $this->assertEquals(64, imagesy($preview)); + + imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/cape.png')); + $preview = Minecraft::generatePreviewFromCape( + file_get_contents(vfsStream::url('root/cape.png')), + 64, + 281, + 250 + ); + $this->assertEquals(281, imagesx($preview)); + $this->assertEquals(250, imagesy($preview)); } }