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:
parent
334eb7c3c5
commit
0a307e9221
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -5,64 +5,72 @@ namespace App\Services;
|
||||
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
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user