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.
This commit is contained in:
printempw 2018-06-28 21:55:33 +08:00
parent 334eb7c3c5
commit 0a307e9221
3 changed files with 139 additions and 117 deletions

View File

@ -102,9 +102,8 @@ class TextureController extends Controller
if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) { if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) {
return $responses[0]; // @codeCoverageIgnore return $responses[0]; // @codeCoverageIgnore
} else { } 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(); ob_start();
imagepng($png); imagepng($png);
imagedestroy($png); imagedestroy($png);
@ -141,24 +140,20 @@ class TextureController extends Controller
if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) { if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) {
return $responses[0]; // @codeCoverageIgnore return $responses[0]; // @codeCoverageIgnore
} else { } else {
$filename = config('filesystems.disks.textures.root').'/'.$t->hash; $binary = Storage::disk('textures')->read($t->hash);
if ($t->type == "cape") { if ($t->type == "cape") {
$png = Minecraft::generatePreviewFromCape($filename, $size); $png = Minecraft::generatePreviewFromCape($binary, $size*0.8, $size*1.125, $size);
ob_start();
imagepng($png);
imagedestroy($png);
$image = ob_get_contents();
ob_end_clean();
} else { } else {
$png = Minecraft::generatePreviewFromSkin($filename, $size, false, false, 4, $t->type == 'alex'); $png = Minecraft::generatePreviewFromSkin($binary, $size, ($t->type == 'alex'), 'both', 4);
ob_start();
imagepng($png);
imagedestroy($png);
$image = ob_get_contents();
ob_end_clean();
} }
ob_start();
imagepng($png);
imagedestroy($png);
$image = ob_get_contents();
ob_end_clean();
return Response::png($image); return Response::png($image);
} }
} }

View File

@ -5,64 +5,72 @@ namespace App\Services;
class Minecraft class Minecraft
{ {
/** /**
* Cut and resize to get avatar from skin, HD support by <xfl03@hotmail.com> * Cut and resize to get the head part from a skin image.
* HD skin support added by xfl03 <xfl03@hotmail.com>.
* *
* @author https://github.com/jamiebicknell/Minecraft-Avatar/blob/master/face.php * @see https://github.com/jamiebicknell/Minecraft-Avatar/blob/master/face.php
* @param string $resource, img path or base64 * @param string $binary Binary image data or decoded base64 formatted image.
* @param int $size * @param int $height The height of generated image in pixel.
* @param string $view, default for 'f' * @param string $view Which side of head to be captured, defaults to 'f' for front view.
* @param bool $base64, if given $resource is encoded in base64
* @return resource * @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); $src = imagecreatefromstring($binary);
$dest = imagecreatetruecolor($size, $size); $dest = imagecreatetruecolor($height, $height);
$ratio = imagesx($src) / 64; // width/64 $ratio = imagesx($src) / 64;
// f => front, l => left, r => right, b => back $x = [
$x = array('f' => 8, 'l' => 16, 'r' => 0, 'b' => 24); '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 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 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] + 32) * $ratio, 8 * $ratio, $height, $height, 8 * $ratio, 8 * $ratio); // Accessories
imagedestroy($src); imagedestroy($src);
return $dest; 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 * @see https://github.com/NC22/Minecraft-HD-skin-viewer-2D/blob/master/SkinViewer2D.class.php
* @param string $resource * @param string $binary Binary image data or decoded base64 formatted image.
* @param int $size * @param int $height The height of generated image in pixel.
* @param bool|string $side 'front' or 'back' * @param bool $alex Whether the given skin is in Alex model.
* @param bool $base64 Generate image from base64 string * @param string $side Which side of model to be captured, 'front', 'back' or 'both'.
* @param int $gap Gap size between front & back preview * @param int $gap Gap size between front & back preview in relative pixel.
* @return resource * @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; $ratio = imagesx($src) / 64;
/** // Check if given skin contains double layers
* Check if double layer skin given
* @var bool
*/
$double = imagesy($src) == 64 * $ratio; $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 if ($side == 'both') {
$half_width = ($side) ? 0 : (($side) ? 8 : 16) * $ratio + $gap * $ratio; // 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); $transparent = imagecolorallocatealpha($dest, 255, 255, 255, 127);
imagefill($dest, 0, 0, $transparent); 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, 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, 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 imagecopy($dest, $src, 4 * $ratio, 8 * $ratio, 20 * $ratio, 20 * $ratio, 8 * $ratio, 12 * $ratio); // Body - 1
@ -97,20 +105,20 @@ class Minecraft
} else { } else {
// I am not sure whether there are single layer Alex-model skin. // I am not sure whether there are single layer Alex-model skin.
if ($alex) { 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 { } 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') { 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, 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 + 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 + 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 imagecopy($dest, $src, $half_width + 4 * $ratio, 0 * $ratio, 56 * $ratio, 8 * $ratio, 8 * $ratio, 8 * $ratio); // Headwear
if ($alex) { 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 + 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); imagecopy($dest, $src, $half_width + 4 * $ratio, 20 * $ratio, 12 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio);
} else { } else {
self::imageflip($dest, $src, $half_width + 0 * $ratio, 8 * $ratio, 52 * $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);
self::imageflip($dest, $src, $half_width + 4 * $ratio, 20 * $ratio, 12 * $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); $width = ($side == 'both') ? $height / 32 * (32 + $gap) : $height / 2;
$fullsize = imagecreatetruecolor($size_x, $size);
$fullsize = imagecreatetruecolor($width, $height);
imagesavealpha($fullsize, true); imagesavealpha($fullsize, true);
$transparent = imagecolorallocatealpha($fullsize, 255, 255, 255, 127); $transparent = imagecolorallocatealpha($fullsize, 255, 255, 255, 127);
imagefill($fullsize, 0, 0, $transparent); imagefill($fullsize, 0, 0, $transparent);
imagecopyresized($fullsize, $dest, 0, 0, 0, 0, imagesx($fullsize), imagesy($fullsize), imagesx($dest), imagesy($dest)); imagecopyresized($fullsize, $dest, 0, 0, 0, 0, imagesx($fullsize), imagesy($fullsize), imagesx($dest), imagesy($dest));
imagedestroy($dest); imagedestroy($dest);
@ -158,37 +165,50 @@ class Minecraft
return $fullsize; 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) $src = imagecreatefromstring($binary);
$size_x = imagesx($img); $ratio = imagesx($src) / 64;
if ($size_y < 1) $width = $height / 16 * 10;
$size_y = imagesy($img);
$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); 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;
}
} }

View File

@ -18,20 +18,17 @@ class MinecraftTest extends TestCase
public function testGenerateAvatarFromSkin() public function testGenerateAvatarFromSkin()
{ {
imagepng(imagecreatetruecolor(64, 32), vfsStream::url('root/skin.png')); 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, imagesx($avatar));
$this->assertEquals(50, imagesy($avatar)); $this->assertEquals(50, imagesy($avatar));
imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/skin.png')); 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, imagesx($avatar));
$this->assertEquals(50, imagesy($avatar)); $this->assertEquals(50, imagesy($avatar));
$avatar = Minecraft::generateAvatarFromSkin( $avatar = Minecraft::generateAvatarFromSkin(
TextureController::getDefaultSteveSkin(), base64_decode(TextureController::getDefaultSteveSkin()), 50, 'l'
50,
'f',
true
); );
$this->assertEquals(50, imagesx($avatar)); $this->assertEquals(50, imagesx($avatar));
$this->assertEquals(50, imagesy($avatar)); $this->assertEquals(50, imagesy($avatar));
@ -40,49 +37,49 @@ class MinecraftTest extends TestCase
public function testGeneratePreviewFromSkin() public function testGeneratePreviewFromSkin()
{ {
imagepng(imagecreatetruecolor(64, 32), vfsStream::url('root/skin.png')); 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(25, imagesx($preview));
$this->assertEquals(50, imagesy($preview)); $this->assertEquals(50, imagesy($preview));
imagepng(imagecreatetruecolor(64, 32), vfsStream::url('root/skin.png')); imagepng(imagecreatetruecolor(64, 32), vfsStream::url('root/skin.png'));
$preview = Minecraft::generatePreviewFromSkin( $preview = Minecraft::generatePreviewFromSkin(
vfsStream::url('root/skin.png'), file_get_contents(vfsStream::url('root/skin.png')),
50, 50,
false, true, // Alex model
false, 'both',
4, 4
true // Alex model
); );
$this->assertEquals(56, imagesx($preview)); $this->assertEquals(56, imagesx($preview));
$this->assertEquals(50, imagesy($preview)); $this->assertEquals(50, imagesy($preview));
imagepng(imagecreatetruecolor(64, 64), vfsStream::url('root/skin.png')); imagepng(imagecreatetruecolor(64, 64), vfsStream::url('root/skin.png'));
$preview = Minecraft::generatePreviewFromSkin( $preview = Minecraft::generatePreviewFromSkin(
vfsStream::url('root/skin.png'), file_get_contents(vfsStream::url('root/skin.png')),
50, 100,
false, true, // Alex model
false, 'both',
4, 8
true // Alex model
); );
$this->assertEquals(56, imagesx($preview)); $this->assertEquals(125, imagesx($preview));
$this->assertEquals(50, imagesy($preview)); $this->assertEquals(100, imagesy($preview));
imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/skin.png')); 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(56, imagesx($preview));
$this->assertEquals(50, imagesy($preview)); $this->assertEquals(50, imagesy($preview));
imagepng(imagecreatetruecolor(128, 128), vfsStream::url('root/skin.png')); 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(56, imagesx($preview));
$this->assertEquals(50, imagesy($preview)); $this->assertEquals(50, imagesy($preview));
$preview = Minecraft::generatePreviewFromSkin( $preview = Minecraft::generatePreviewFromSkin(
TextureController::getDefaultSteveSkin(), base64_decode(TextureController::getDefaultSteveSkin()),
50, 50,
true, false,
true 'back'
); );
$this->assertEquals(25, imagesx($preview)); $this->assertEquals(25, imagesx($preview));
$this->assertEquals(50, imagesy($preview)); $this->assertEquals(50, imagesy($preview));
@ -91,8 +88,18 @@ class MinecraftTest extends TestCase
public function testGeneratePreviewFromCape() public function testGeneratePreviewFromCape()
{ {
imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/cape.png')); imagepng(imagecreatetruecolor(128, 64), vfsStream::url('root/cape.png'));
$preview = Minecraft::generatePreviewFromCape(vfsStream::url('root/cape.png')); $preview = Minecraft::generatePreviewFromCape(file_get_contents(vfsStream::url('root/cape.png')), 64);
$this->assertEquals(250, imagesx($preview)); $this->assertEquals(40, imagesx($preview));
$this->assertEquals(166, imagesy($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));
} }
} }