switch server-side texture renderer

This commit is contained in:
Pig Fang 2020-01-12 09:27:39 +08:00
parent 6c221c28d6
commit b1ccdb47f2
34 changed files with 328 additions and 829 deletions

View File

@ -1,25 +0,0 @@
<?php
namespace App\Events;
use App\Models\Texture;
class GetAvatarPreview extends Event
{
public $size;
public $texture;
/**
* Create a new event instance.
*
* @param int $size
*
* @return void
*/
public function __construct(Texture $texture, $size)
{
$this->texture = $texture;
$this->size = $size;
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Events;
use App\Models\Texture;
class GetSkinPreview extends Event
{
public $size;
public $texture;
/**
* Create a new event instance.
*
* @param int $size
*
* @return void
*/
public function __construct(Texture $texture, $size)
{
$this->texture = $texture;
$this->size = $size;
}
}

View File

@ -2,25 +2,31 @@
namespace App\Http\Controllers;
use App\Events\GetAvatarPreview;
use App\Events\GetSkinPreview;
use App\Models\Player;
use App\Models\Texture;
use App\Models\User;
use App\Services\Minecraft;
use Blessing\Minecraft;
use Cache;
use Carbon\Carbon;
use Event;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Image;
use Option;
use Response;
use Storage;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
class TextureController extends Controller
{
public function __construct()
{
$this->middleware('cache.headers:etag;public;max_age='.option('cache_expire_time'))
->only([
'preview',
'raw',
'texture',
'avatarByPlayer',
'avatarByUser',
'avatarByTexture',
]);
}
public function json($player)
{
$player = $this->getPlayerInstance($player);
@ -37,144 +43,112 @@ class TextureController extends Controller
return response()->json($player)->setLastModified($player->last_modified);
}
public function texture($hash, $headers = [], $message = '')
public function preview(Minecraft $minecraft, Request $request, $tid)
{
try {
if (Storage::disk('textures')->has($hash)) {
return $this->outputImage(Storage::disk('textures')->get($hash), array_merge([
'Last-Modified' => Storage::disk('textures')->lastModified($hash),
'Accept-Ranges' => 'bytes',
'Content-Length' => Storage::disk('textures')->size($hash),
], $headers));
}
} catch (Exception $e) {
report($e);
}
$texture = Texture::findOrFail($tid);
$hash = $texture->hash;
return abort(404, $message);
}
$disk = Storage::disk('textures');
abort_if($disk->missing($hash), 404);
public function avatarByTid($tid, $size = 128)
{
if ($t = Texture::find($tid)) {
try {
if (Storage::disk('textures')->has($t->hash)) {
$responses = event(new GetAvatarPreview($t, $size));
if (isset($responses[0]) && $responses[0] instanceof SymfonyResponse) {
return $responses[0]; // @codeCoverageIgnore
} else {
$png = Minecraft::generateAvatarFromSkin(
Storage::disk('textures')->read($t->hash), $size
);
return $this->outputImage(png($png));
}
$height = (int) $request->query('height', 200);
$now = Carbon::now();
$response = Cache::remember(
'preview-t'.$tid,
option('enable_preview_cache') ? $now->addYear() : $now->addMinute(),
function () use ($minecraft, $disk, $texture, $hash, $height) {
$file = $disk->get($hash);
if ($texture->type === 'cape') {
$image = $minecraft->renderCape($file, 12);
} else {
$image = $minecraft->renderSkin($file, 12, $texture->type === 'alex');
}
} catch (Exception $e) {
report($e);
$lastModified = $disk->lastModified($hash);
return Image::make($image)
->resize(null, $height, function ($constraint) {
$constraint->aspectRatio();
})
->response('png', 100)
->setLastModified(Carbon::createFromTimestamp($lastModified));
}
}
);
$default = Image::make(storage_path('static_textures/avatar.png'));
return $default->resize($size, $size)->response();
}
public function avatar($uid, $size = 128)
{
$user = User::find($uid);
if ($user) {
return $this->avatarByTid($user->avatar, $size);
}
$default = Image::make(storage_path('static_textures/avatar.png'));
return $default->resize($size, $size)->response();
}
public function preview($tid, $size = 250)
{
if ($t = Texture::find($tid)) {
try {
if (Storage::disk('textures')->has($t->hash)) {
$responses = event(new GetSkinPreview($t, $size));
if (isset($responses[0]) && $responses[0] instanceof \Symfony\Component\HttpFoundation\Response) {
return $responses[0]; // @codeCoverageIgnore
} else {
$binary = Storage::disk('textures')->read($t->hash);
if ($t->type == 'cape') {
$png = Minecraft::generatePreviewFromCape(
$binary, $size * 0.8, $size * 1.125, $size
);
} else {
$png = Minecraft::generatePreviewFromSkin(
$binary, $size, ($t->type == 'alex'), 'both', 4
);
}
return $this->outputImage(png($png));
}
}
} catch (Exception $e) {
report($e);
}
}
// Show this if given texture is invalid.
return response()->file(storage_path('static_textures/broken.png'));
return $response;
}
public function raw($tid)
{
abort_unless(option('allow_downloading_texture'), 404);
abort_unless(option('allow_downloading_texture'), 403);
return ($t = Texture::find($tid))
? $this->texture($t->hash)
: abort(404, trans('skinlib.non-existent'));
$texture = Texture::findOrFail($tid);
return $this->texture($texture->hash);
}
public function avatarByPlayer($size, $name)
public function texture(string $hash)
{
$player = Player::where('name', $name)->first();
abort_unless($player, 404);
$disk = Storage::disk('textures');
abort_if($disk->missing($hash), 404);
$hash = optional($player->skin)->hash;
if (Storage::disk('textures')->has($hash)) {
$png = Minecraft::generateAvatarFromSkin(
Storage::disk('textures')->read($hash),
$size
);
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
return $this->outputImage(png($png));
}
return abort(404);
return response($disk->get($hash))
->withHeaders([
'Content-Type' => 'image/png',
'Content-Length' => $disk->size($hash),
])
->setLastModified($lastModified);
}
protected function outputImage($content, $headers = [])
public function avatarByPlayer(Minecraft $minecraft, Request $request, $name)
{
$request = request();
$player = Player::where('name', $name)->firstOrFail();
$ifNoneMatch = $request->header('If-None-Match');
$eTag = md5($content);
return $this->avatar($minecraft, $player->skin, (int) $request->query('size', 100));
}
$ifModifiedSince = Carbon::parse($request->header('If-Modified-Since', 0));
$lastModified = Carbon::parse(Arr::pull($headers, 'Last-Modified', time()));
public function avatarByUser(Minecraft $minecraft, Request $request, $uid)
{
$texture = Texture::find(optional(User::find($uid))->avatar);
if ($eTag === $ifNoneMatch || $lastModified <= $ifModifiedSince) {
return response(null)->withHeaders($headers)->setNotModified();
return $this->avatar($minecraft, $texture, (int) $request->query('size', 100));
}
public function avatarByTexture(Minecraft $minecraft, Request $request, $tid)
{
$texture = Texture::find($tid);
return $this->avatar($minecraft, $texture, (int) $request->query('size', 100));
}
protected function avatar(Minecraft $minecraft, Texture $texture = null, int $size = 100)
{
$disk = Storage::disk('textures');
if (is_null($texture) || $disk->missing($texture->hash)) {
return Image::make(storage_path('static_textures/avatar.png'))
->resize($size, $size)
->response('png', 100);
}
return response($content, 200, $headers)->withHeaders([
'Content-Type' => 'image/png',
'ETag' => $eTag,
'Last-Modified' => $lastModified->toRfc7231String(),
'Cache-Control' => 'max-age='.option('cache_expire_time').', public',
]);
$hash = $texture->hash;
$now = Carbon::now();
$response = Cache::remember(
'avatar-2d-t'.$texture->tid.'-s'.$size,
option('enable_avatar_cache') ? $now->addYear() : $now->addMinute(),
function () use ($minecraft, $disk, $hash, $size) {
$image = $minecraft->render2dAvatar($disk->get($hash), 25);
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
return Image::make($image)
->resize($size, $size)
->response('png', 100)
->setLastModified($lastModified);
}
);
return $response;
}
protected function getPlayerInstance($player_name)
@ -184,16 +158,4 @@ class TextureController extends Controller
return $player;
}
/**
* Default steve skin, base64 encoded.
*
* @see https://minecraft.gamepedia.com/File:Steve_skin.png
*
* @return string
*/
public static function getDefaultSteveSkin()
{
return 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFDUlEQVR42u2a20sUURzH97G0LKMotPuWbVpslj1olJXdjCgyisowsSjzgrB0gSKyC5UF1ZNQWEEQSBQ9dHsIe+zJ/+nXfM/sb/rN4ZwZ96LOrnPgyxzP/M7Z+X7OZc96JpEISfWrFhK0YcU8knlozeJKunE4HahEqSc2nF6zSEkCgGCyb+82enyqybtCZQWAzdfVVFgBJJNJn1BWFgC49/VpwGVlD0CaxQiA5HSYEwBM5sMAdKTqygcAG9+8coHKY/XXAZhUNgDYuBSPjJL/GkzVVhAEU5tqK5XZ7cnFtHWtq/TahdSw2l0HUisr1UKIWJQBAMehDuqiDdzndsP2EZECAG1ZXaWMwOCODdXqysLf++uXUGv9MhUHIByDOijjdiSAoH3ErANQD73C7TXXuGOsFj1d4YH4OTJAEy8y9Hd0mCaeZ5z8dfp88zw1bVyiYhCLOg1ZeAqC0ybaDttHRGME1DhDeVWV26u17lRAPr2+mj7dvULfHw2q65fhQRrLXKDfIxkau3ZMCTGIRR3URR5toU38HbaPiMwUcKfBAkoun09PzrbQ2KWD1JJaqswjdeweoR93rirzyCMBCmIQizqoizZkm2H7iOgAcHrMHbbV9KijkUYv7qOn55sdc4fo250e+vUg4329/Xk6QB/6DtOws+dHDGJRB3XRBve+XARt+4hIrAF4UAzbnrY0ve07QW8uHfB+0LzqanMM7qVb+3f69LJrD90/1axiEIs6qIs21BTIToewfcSsA+Bfb2x67OoR1aPPzu2i60fSNHRwCw221Suz0O3jO+jh6V1KyCMGse9721XdN5ePutdsewxS30cwuMjtC860T5JUKpXyKbSByUn7psi5l+juDlZYGh9324GcPKbkycaN3jUSAGxb46IAYPNZzW0AzgiQ5tVnzLUpUDCAbakMQXXrOtX1UMtHn+Q9/X5L4wgl7t37r85OSrx+TYl379SCia9KXjxRpiTjIZTBFOvrV1f8ty2eY/T7XJ81FQAwmA8ASH1ob68r5PnBsxA88/xAMh6SpqW4HRnLBrkOA9Xv5wPAZjAUgOkB+SHxgBgR0qSMh0zmZRsmwDJm1gFg2PMDIC8/nAHIMls8x8GgzOsG5WiaqREgYzDvpTwjLDy8NM15LpexDEA3LepjU8Z64my+8PtDCmUyRr+fFwA2J0eAFYA0AxgSgMmYBMZTwFQnO9RNAEaHOj2DXF5UADmvAToA2ftyxZYA5BqgmZZApDkdAK4mAKo8GzPlr8G8AehzMAyA/i1girUA0HtYB2CaIkUBEHQ/cBHSvwF0AKZFS5M0ZwMQtEaEAmhtbSUoDADH9ff3++QZ4o0I957e+zYAMt6wHkhzpjkuAcgpwNcpA7AZDLsvpwiuOkBvxygA6Bsvb0HlaeKIF2EbADZpGiGzBsA0gnwQHGOhW2snRpbpPexbAB2Z1oicAMQpTnGKU5ziFKc4xSlOcYpTnOIUpzgVmgo+XC324WfJAdDO/+ceADkCpuMFiFKbApEHkOv7BfzfXt+5gpT8V7rpfYJcDz+jAsB233r6yyBsJ0mlBCDofuBJkel4vOwBFPv8fyYAFPJ+wbSf/88UANNRVy4Awo6+Ig2gkCmgA5DHWjoA+X7AlM//owLANkX0w0359od++pvX8fdMAcj3/QJ9iJsAFPQCxHSnQt8vMJ3v2wCYpkhkAOR7vG7q4aCXoMoSgG8hFAuc/grMdAD4B/kHl9da7Ne9AAAAAElFTkSuQmCC';
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Services\PackageManager;
use Cache;
use Composer\CaBundle\CaBundle;
use Composer\Semver\Comparator;
use Exception;
@ -75,6 +76,7 @@ class UpdateController extends Controller
$artisan->call('migrate', ['--force' => true]);
$artisan->call('view:clear');
$filesystem->put(storage_path('install.lock'), '');
Cache::flush();
return view('setup.updates.success');
}

View File

@ -58,6 +58,7 @@ class Kernel extends HttpKernel
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'verified' => \App\Http\Middleware\CheckUserVerified::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'admin' => \App\Http\Middleware\CheckAdministrator::class,

View File

@ -24,7 +24,7 @@ class UserMenuComposer
{
$user = auth()->user();
$email = base64_encode($user->email);
$avatarUrl = url('/avatar/user/'.$user->uid.'/25');
$avatarUrl = url('/avatar/user/'.$user->uid.'?size=25');
$avatar = $this->filter->apply('user_avatar', $avatarUrl, [$user]);
$view->with(compact('user', 'avatar'));

View File

@ -23,7 +23,7 @@ class UserPanelComposer
public function compose(View $view)
{
$user = auth()->user();
$avatarUrl = url('/avatar/user/'.$user->uid.'/45');
$avatarUrl = url('/avatar/user/'.$user->uid.'?size=45');
$avatar = $this->filter->apply('user_avatar', $avatarUrl, [$user]);
$badges = [];

View File

@ -1,25 +0,0 @@
<?php
namespace App\Listeners;
use App\Services\Minecraft;
use Cache;
use Storage;
class CacheAvatarPreview
{
public function handle($event)
{
$texture = $event->texture;
$size = $event->size;
$key = "avatar-{$texture->tid}-$size";
$content = Cache::rememberForever($key, function () use ($texture, $size) {
$res = Storage::disk('textures')->read($texture->hash);
return png(Minecraft::generateAvatarFromSkin($res, $size));
});
return response($content, 200, ['Content-Type' => 'image/png']);
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Listeners;
use App\Services\Minecraft;
use Cache;
use Storage;
class CacheSkinPreview
{
public function handle($event)
{
$texture = $event->texture;
$size = $event->size;
$key = "preview-{$texture->tid}-{$size}";
$content = Cache::rememberForever($key, function () use ($texture, $size) {
$res = Storage::disk('textures')->read($texture->hash);
if ($texture->type == 'cape') {
$png = Minecraft::generatePreviewFromCape($res, $size * 0.8, $size * 1.125, $size);
} else {
$png = Minecraft::generatePreviewFromSkin($res, $size, $texture->type == 'alex', 'both', 4);
}
return png($png);
});
return response($content, 200, [
'Content-Type' => 'image/png',
'Last-Modified' => Storage::disk('textures')->lastModified($texture->hash),
]);
}
}

View File

@ -46,12 +46,6 @@ class EventServiceProvider extends ServiceProvider
{
parent::boot();
if (option('enable_avatar_cache')) {
Event::listen(Events\GetAvatarPreview::class, Listeners\CacheAvatarPreview::class);
}
if (option('enable_preview_cache')) {
Event::listen(Events\GetSkinPreview::class, Listeners\CacheSkinPreview::class);
}
if (option('enable_notfound_cache')) {
Event::subscribe(Listeners\CachePlayerExists::class);
}

View File

@ -1,218 +0,0 @@
<?php
namespace App\Services;
class Minecraft
{
/**
* Cut and resize to get the head part from a skin image.
* HD skin support added by xfl03 <xfl03@hotmail.com>.
*
* @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(string $binary, int $height, string $view = 'f')
{
$src = imagecreatefromstring($binary);
$dest = imagecreatetruecolor($height, $height);
$ratio = imagesx($src) / 64;
$x = [
'f' => 8, // Front
'l' => 16, // Left
'r' => 0, // Right
'b' => 24, // Back
];
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 a image preview for a skin texture.
*
* @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(string $binary, int $height, $alex = false, $side = 'both', $gap = 4)
{
$src = imagecreatefromstring($binary);
$ratio = imagesx($src) / 64;
// Check if given skin contains double layers
$double = imagesy($src) == 64 * $ratio;
$dest = imagecreatetruecolor((32 + $gap) * $ratio, 32 * $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 == '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
imagecopy($dest, $src, 4 * $ratio, 20 * $ratio, 4 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Right Leg - 1
if ($alex) {
imagecopy($dest, $src, 1 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 3 * $ratio, 12 * $ratio); // Right Arm - 1
} else {
imagecopy($dest, $src, 0 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Right Arm - 1
}
// Check if given skin is double layer skin.
// If not, flip right arm/leg to generate left arm/leg.
if ($double) {
imagecopy($dest, $src, 8 * $ratio, 20 * $ratio, 20 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio); // Left Leg - 1
// copy second layer
imagecopy($dest, $src, 4 * $ratio, 8 * $ratio, 20 * $ratio, 36 * $ratio, 8 * $ratio, 12 * $ratio); // Body - 2
imagecopy($dest, $src, 4 * $ratio, 20 * $ratio, 4 * $ratio, 36 * $ratio, 4 * $ratio, 12 * $ratio); // Right Leg - 2
imagecopy($dest, $src, 8 * $ratio, 20 * $ratio, 4 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio); // Left Leg - 2
if ($alex) {
imagecopy($dest, $src, 12 * $ratio, 8 * $ratio, 36 * $ratio, 52 * $ratio, 3 * $ratio, 12 * $ratio); // Left Arm - 1
imagecopy($dest, $src, 1 * $ratio, 8 * $ratio, 44 * $ratio, 36 * $ratio, 3 * $ratio, 12 * $ratio); // Right Arm - 2
imagecopy($dest, $src, 11 * $ratio, 8 * $ratio, 50 * $ratio, 52 * $ratio, 3 * $ratio, 12 * $ratio); // Left Arm - 2
} else {
imagecopy($dest, $src, 12 * $ratio, 8 * $ratio, 36 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio); // Left Arm - 1
imagecopy($dest, $src, 0 * $ratio, 8 * $ratio, 44 * $ratio, 36 * $ratio, 4 * $ratio, 12 * $ratio); // Right Arm - 2
imagecopy($dest, $src, 12 * $ratio, 8 * $ratio, 52 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio); // Left Arm - 2
}
} else {
// I am not sure whether there are single layer Alex-model skin.
if ($alex) {
static::imageflip($dest, $src, 12 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 3 * $ratio, 12 * $ratio); // Left Arm
} else {
static::imageflip($dest, $src, 12 * $ratio, 8 * $ratio, 44 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Left Arm
}
static::imageflip($dest, $src, 8 * $ratio, 20 * $ratio, 4 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Left Leg
}
}
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) {
imagecopy($dest, $src, $half_width + 12 * $ratio, 8 * $ratio, 51 * $ratio, 20 * $ratio, 3 * $ratio, 12 * $ratio); // Right Arm
} else {
imagecopy($dest, $src, $half_width + 12 * $ratio, 8 * $ratio, 52 * $ratio, 20 * $ratio, 4 * $ratio, 12 * $ratio); // Right Arm
}
if ($double) {
if ($alex) {
imagecopy($dest, $src, $half_width + 1 * $ratio, 8 * $ratio, 43 * $ratio, 52 * $ratio, 3 * $ratio, 12 * $ratio);
} else {
imagecopy($dest, $src, $half_width + 0 * $ratio, 8 * $ratio, 44 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio);
}
imagecopy($dest, $src, $half_width + 4 * $ratio, 20 * $ratio, 28 * $ratio, 52 * $ratio, 4 * $ratio, 12 * $ratio); // Left Leg
// copy second layer
imagecopy($dest, $src, $half_width + 4 * $ratio, 8 * $ratio, 32 * $ratio, 36 * $ratio, 8 * $ratio, 12 * $ratio);
imagecopy($dest, $src, $half_width + 12 * $ratio, 8 * $ratio, 52 * $ratio, 36 * $ratio, 4 * $ratio, 12 * $ratio);
if ($alex) {
imagecopy($dest, $src, $half_width + 1 * $ratio, 8 * $ratio, 59 * $ratio, 52 * $ratio, 3 * $ratio, 12 * $ratio);
} else {
imagecopy($dest, $src, $half_width + 0 * $ratio, 8 * $ratio, 60 * $ratio, 52 * $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);
} else {
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);
}
}
$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);
imagedestroy($src);
return $fullsize;
}
/**
* 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(string $binary, int $height, $fillWidth = 0, $fillHeight = 0)
{
$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);
}
}

View File

@ -82,16 +82,3 @@ if (!function_exists('option_localized')) {
return option($key.'_'.config('app.locale'), option($key));
}
}
if (!function_exists('png')) {
function png($resource)
{
ob_start();
imagepng($resource);
$image = ob_get_contents();
ob_end_clean();
imagedestroy($resource);
return $image;
}
}

View File

@ -15,6 +15,7 @@
"ext-zip": "*",
"blessing/filter": "^1.0",
"blessing/rejection": "^1.0",
"blessing/texture-renderer": "^0.1.1",
"composer/ca-bundle": "^1.2",
"composer/semver": "^1.4",
"doctrine/dbal": "^2.9",

39
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7fffc88319cd5ed6dbadbf6b52564465",
"content-hash": "938107f4ff375749aa599b1e4069f2d2",
"packages": [
{
"name": "blessing/filter",
@ -98,6 +98,42 @@
"description": "Rejection is an object that indicates you are rejecting.",
"time": "2019-12-31T09:26:32+00:00"
},
{
"name": "blessing/texture-renderer",
"version": "0.1.1",
"source": {
"type": "git",
"url": "https://github.com/bs-community/texture-renderer.git",
"reference": "26dc3d8c516f8bd106a41ff3d534cbe2652a1753"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bs-community/texture-renderer/zipball/26dc3d8c516f8bd106a41ff3d534cbe2652a1753",
"reference": "26dc3d8c516f8bd106a41ff3d534cbe2652a1753",
"shasum": ""
},
"require": {
"intervention/image": "^2.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Blessing\\": "src/Blessing"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Pig Fang",
"email": "g-plane@hotmail.com"
}
],
"description": "Minecraft texture renderer.",
"time": "2020-01-10T08:35:52+00:00"
},
{
"name": "composer/ca-bundle",
"version": "1.2.5",
@ -5190,6 +5226,7 @@
"psr",
"psr-7"
],
"abandoned": "laminas/laminas-diactoros",
"time": "2019-11-13T19:16:13+00:00"
}
],

View File

@ -92,7 +92,7 @@ export default {
}
},
avatarUrl(player) {
return `${blessing.base_url}/avatar/${player.tid_skin}/35`
return `${blessing.base_url}/avatar/${player.tid_skin}?size=35`
},
},
}

View File

@ -76,7 +76,7 @@ export default {
},
computed: {
previewLink() {
return `${blessing.base_url}/preview/${this.tid}.png`
return `${blessing.base_url}/preview/${this.tid}?height=150`
},
linkToSkinlib() {
return `${blessing.base_url}/skinlib/show/${this.tid}`

View File

@ -60,7 +60,7 @@ export default {
return `${blessing.base_url}/skinlib/show/${this.tid}`
},
urlToPreview() {
return `${blessing.base_url}/preview/${this.tid}.png`
return `${blessing.base_url}/preview/${this.tid}?height=150`
},
likeActionText() {
if (this.anonymous) {
@ -102,6 +102,8 @@ export default {
.texture-img
background #eff1f0
img
height 210px
.btn-like
color #6c757d

View File

@ -36,13 +36,13 @@
v-if="props.row.tid_skin"
:href="`${baseUrl}/skinlib/show/${props.row.tid_skin}`"
>
<img :src="`${baseUrl}/preview/${props.row.tid_skin}/64`" width="64">
<img :src="`${baseUrl}/preview/${props.row.tid_skin}?height=64`" width="64">
</a>
<a
v-if="props.row.tid_cape"
:href="`${baseUrl}/skinlib/show/${props.row.tid_cape}`"
>
<img :src="`${baseUrl}/preview/${props.row.tid_cape}/64`" width="64">
<img :src="`${baseUrl}/preview/${props.row.tid_cape}?height=64`" width="64">
</a>
</span>
<span v-else-if="props.column.field === 'operations'">

View File

@ -312,7 +312,7 @@ export default {
},
download() {
const a = document.createElement('a')
a.href = `${this.baseUrl}/raw/${this.tid}.png`
a.href = `${this.baseUrl}/raw/${this.tid}`
a.download = `${this.name}.png`
const event = new MouseEvent('click')
a.dispatchEvent(event)

View File

@ -75,7 +75,7 @@
<a v-if="preview2d.skin" :href="`${baseUrl}/skinlib/show/${preview2d.skin}`">
<img
class="skin2d"
:src="`${baseUrl}/preview/${preview2d.skin}/64`"
:src="`${baseUrl}/preview/${preview2d.skin}?height=128`"
>
</a>
<span v-else v-t="'user.player.texture-empty'" class="skin2d" />
@ -86,7 +86,7 @@
<a v-if="preview2d.cape" :href="`${baseUrl}/skinlib/show/${preview2d.cape}`">
<img
class="skin2d"
:src="`${baseUrl}/preview/${preview2d.cape}/64`"
:src="`${baseUrl}/preview/${preview2d.cape}?height=128`"
>
</a>
<span v-else v-t="'user.player.texture-empty'" class="skin2d" />

View File

@ -48,5 +48,5 @@ test('compute avatar URL', () => {
// eslint-disable-next-line camelcase
const wrapper = mount<Vue & { avatarUrl(player: { tid_skin: number }): string }>(ApplyToPlayerDialog)
const { avatarUrl } = wrapper.vm
expect(avatarUrl({ tid_skin: 1 })).toBe('/avatar/1/35')
expect(avatarUrl({ tid_skin: 1 })).toBe('/avatar/1?size=35')
})

View File

@ -17,7 +17,7 @@ function factory(opt = {}) {
test('computed values', () => {
const wrapper = mount(ClosetItem, { propsData: factory() })
expect(wrapper.find('img').attributes('src')).toBe('/preview/1.png')
expect(wrapper.find('img').attributes('src')).toBe('/preview/1?height=150')
expect(
wrapper
.findAll('.dropdown-item')

View File

@ -11,7 +11,7 @@ test('urls', () => {
propsData: { tid: 1 },
})
expect(wrapper.find('a').attributes('href')).toBe('/skinlib/show/1')
expect(wrapper.find('img').attributes('src')).toBe('/preview/1.png')
expect(wrapper.find('img').attributes('src')).toBe('/preview/1?height=150')
})
test('render basic information', () => {

View File

@ -45,7 +45,7 @@ test('change texture', async () => {
)
modal.vm.$emit('confirm')
await flushPromises()
expect(wrapper.html()).toContain('/preview/5/64')
expect(wrapper.html()).toContain('/preview/5?height=64')
})
test('change player name', async () => {

View File

@ -210,7 +210,7 @@ test('apply texture', async () => {
button.trigger('click')
await flushPromises()
expect(wrapper.find('input[type="radio"]').attributes('value')).toBe('1')
expect(wrapper.find('.model-label > img').attributes('src')).toBe('/avatar/10/35')
expect(wrapper.find('.model-label > img').attributes('src')).toBe('/avatar/10?size=35')
expect(wrapper.find('.modal-body').text()).toContain('name')
jest.runAllTimers()
})

View File

@ -76,10 +76,10 @@ test('click to preview player', async () => {
expect(Vue.prototype.$http.get).toBeCalledWith('/skinlib/info/2')
wrapper.find('[data-test="to2d"]').trigger('click')
expect(wrapper.find('img').attributes('src')).toBe('/preview/2/64')
expect(wrapper.find('img').attributes('src')).toBe('/preview/2?height=128')
wrapper.find('tbody > tr:nth-child(4)').trigger('click')
await flushPromises()
expect(wrapper.find('img').attributes('src')).toBe('/preview/5/64')
expect(wrapper.find('img').attributes('src')).toBe('/preview/5?height=128')
})
test('change player name', async () => {

View File

@ -7,10 +7,10 @@ Route::group(['middleware' => 'player'], function () {
Route::get('/textures/{hash}', 'TextureController@texture');
Route::get('/avatar/player/{size}/{name}.png', 'TextureController@avatarByPlayer');
Route::get('/avatar/user/{uid}/{size?}', 'TextureController@avatar');
Route::get('/avatar/{tid}/{size?}', 'TextureController@avatarByTid');
Route::get('/avatar/player/{name}', 'TextureController@avatarByPlayer');
Route::get('/avatar/user/{uid}', 'TextureController@avatarByUser');
Route::get('/avatar/{tid}', 'TextureController@avatarByTexture');
Route::get('/raw/{tid}.png', 'TextureController@raw');
Route::get('/raw/{tid}', 'TextureController@raw');
Route::get('/preview/{tid}/{size?}', 'TextureController@preview');
Route::get('/preview/{tid}', 'TextureController@preview');

26
tests/Fakes/Minecraft.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace Tests\Fakes;
class Minecraft extends \Blessing\Minecraft
{
public function renderSkin($skin, $ratio = 7.0, $isAlex = false)
{
return imagecreate(2, 5);
}
public function renderCape($cape, int $height)
{
return imagecreate(1, 2);
}
public function render2dAvatar($skin, $ratio = 15.0)
{
return imagecreate(1, 1);
}
public function render3dAvatar($skin, $ratio = 15.0)
{
return imagecreate(1, 1);
}
}

View File

@ -6,18 +6,21 @@ use App\Models\Player;
use App\Models\Texture;
use App\Models\User;
use Cache;
use Carbon\Carbon;
use Event;
use Exception;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
use Image;
use Mockery;
class TextureControllerTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->swap(\Blessing\Minecraft::class, new Fakes\Minecraft());
}
public function testJson()
{
$steve = factory(Texture::class)->create();
@ -67,161 +70,160 @@ class TextureControllerTest extends TestCase
])->assertHeader('Last-Modified');
}
public function testTexture()
{
Storage::fake('textures');
$steve = factory(Texture::class)->create();
Storage::disk('textures')->put($steve->hash, '');
$this->get('/textures/nope')
->assertSee('404');
$this->get('/textures/'.$steve->hash)
->assertHeader('Content-Type', 'image/png')
->assertHeader('Last-Modified')
->assertHeader('ETag')
->assertHeader('Cache-Control', 'max-age='.option('cache_expire_time').', public')
->assertHeader('Accept-Ranges', 'bytes')
->assertHeader('Content-Length', Storage::disk('textures')->size($steve->hash))
->assertSuccessful();
// Cache test
$this->get('/textures/'.$steve->hash, [
'If-None-Match' => md5(''),
])->assertStatus(304);
$this->get('/textures/'.$steve->hash, [
'If-Modified-Since' => Carbon::now()->addHours(1)->toRfc7231String(),
])->assertStatus(304);
Storage::shouldReceive('disk')->with('textures')->andThrow(new Exception());
$this->get('/textures/'.$steve->hash)->assertNotFound();
}
public function testAvatarByTid()
{
$this->get('/avatar/1')->assertHeader('Content-Type', 'image/png');
}
public function testAvatar()
{
Event::fake();
Storage::fake('textures');
$image = $this->get('/avatar/user/5/45')
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($image);
$this->assertEquals(45, $image->width());
$this->assertEquals(45, $image->height());
$steve = factory(Texture::class)->create();
$png = base64_decode(\App\Http\Controllers\TextureController::getDefaultSteveSkin());
Storage::disk('textures')->put($steve->hash, $png);
$user = factory(User::class)->create(['avatar' => $steve->tid]);
$mock = Mockery::mock('overload:Minecraft');
$mock->shouldReceive('generateAvatarFromSkin')
->once()
->andReturn(imagecreatefromstring($png));
$this->get('/avatar/user/'.$user->uid)
->assertHeader('Content-Type', 'image/png');
Event::assertDispatched(\App\Events\GetAvatarPreview::class);
Storage::shouldReceive('disk')->with('textures')->andThrow(new Exception());
$image = $this->get('/avatar/user/'.$user->uid.'/45')
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($image);
$this->assertEquals(45, $image->width());
$this->assertEquals(45, $image->height());
}
public function testPreview()
{
Event::fake();
Storage::fake('textures');
$disk = Storage::fake('textures');
$this->get('/preview/0')->assertNotFound();
$skin = factory(Texture::class)->create();
$this->get('/preview/'.$skin->tid)->assertNotFound();
$disk->put($skin->hash, '');
$content = $this->get('/preview/'.$skin->tid)
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($content);
$this->assertEquals(80, $image->width());
$this->assertEquals(200, $image->height());
$this->assertTrue(Cache::has('preview-t'.$skin->tid));
$steve = factory(Texture::class)->create();
$cape = factory(Texture::class, 'cape')->create();
$this->get('/preview/0')
->assertHeader('Content-Type', 'image/png');
$this->get("/preview/{$steve->tid}")
->assertHeader('Content-Type', 'image/png');
$png = base64_decode(\App\Http\Controllers\TextureController::getDefaultSteveSkin());
Storage::disk('textures')->put($steve->hash, $png);
Storage::disk('textures')->put($cape->hash, $png);
$mock = Mockery::mock('overload:Minecraft');
$mock->shouldReceive('generatePreviewFromSkin')
->once()
->andReturn(imagecreatefromstring($png));
$this->get("/preview/{$steve->tid}/56")
->assertHeader('Content-Type', 'image/png');
Event::fake(\App\Events\GetSkinPreview::class);
$mock->shouldReceive('generatePreviewFromCape')
->once()
->andReturn(imagecreatefromstring($png));
$this->get("/preview/{$cape->tid}")
->assertHeader('Content-Type', 'image/png');
Storage::shouldReceive('disk')->with('textures')->andThrow(new Exception());
$this->get("/preview/{$steve->tid}")
->assertHeader('Content-Type', 'image/png');
$disk->put($cape->hash, '');
$content = $this->get('/preview/'.$cape->tid.'?height=100')->getContent();
$image = Image::make($content);
$this->assertEquals(50, $image->width());
$this->assertEquals(100, $image->height());
$this->assertTrue(Cache::has('preview-t'.$cape->tid));
}
public function testRaw()
{
Storage::fake('textures');
$steve = factory(Texture::class)->create();
Storage::disk('textures')->put($steve->hash, '');
$disk = Storage::fake('textures');
$skin = factory(Texture::class)->create();
// Not found
$this->get('/raw/0.png')
->assertNotFound()
->assertSee(trans('skinlib.non-existent'));
$this->get('/raw/0')->assertNotFound();
// Missing texture file
$this->get('/raw/'.$skin->tid)->assertNotFound();
// Success
$this->get("/raw/{$steve->tid}.png")
->assertHeader('Content-Type', 'image/png');
// Texture is deleted
Storage::disk('textures')->delete($steve->hash);
$this->get("/raw/{$steve->tid}.png")->assertNotFound();
$disk->put($skin->hash, '');
$this->get('/raw/'.$skin->tid)->assertHeader('Content-Type', 'image/png');
// Disallow downloading texture directly
option(['allow_downloading_texture' => false]);
$this->get("/raw/{$steve->tid}.png")->assertNotFound();
$this->get('/raw/'.$skin->tid)->assertForbidden();
}
public function testTexture()
{
$disk = Storage::fake('textures');
$skin = factory(Texture::class)->create();
$this->get('/textures/'.$skin->hash)->assertNotFound();
$disk->put($skin->hash, '');
$this->get('/textures/'.$skin->hash)
->assertHeader('Content-Type', 'image/png');
}
public function testAvatarByPlayer()
{
Storage::fake('textures');
$disk = Storage::fake('textures');
// No such player.
$this->get('/avatar/player/1/abc.png')->assertNotFound();
$this->get('/avatar/player/abc')->assertNotFound();
// No such texture.
$player = factory(Player::class)->create();
$this->get("/avatar/player/1/{$player->name}.png")->assertNotFound();
$this->get('/avatar/player/'.$player->name)
->assertSuccessful()
->assertHeader('Content-Type', 'image/png');
$texture = factory(Texture::class)->create();
$disk->put($texture->hash, '');
$player->tid_skin = $texture->tid;
$player->save();
$this->get("/avatar/player/1/{$player->name}.png")->assertNotFound();
$image = $this->get('/avatar/player/'.$player->name)
->assertSuccessful()
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($image);
$this->assertEquals(100, $image->width());
$this->assertEquals(100, $image->height());
// Success
$png = base64_decode(\App\Http\Controllers\TextureController::getDefaultSteveSkin());
Storage::disk('textures')->put($texture->hash, $png);
Mockery::mock('overload:Minecraft')
->shouldReceive('generateAvatarFromSkin')
->once()
->andReturn(imagecreatefromstring($png));
$this->get("/avatar/player/20/{$player->name}.png")->assertSuccessful();
Storage::disk('textures')->delete($texture->hash);
$image = $this->get('/avatar/player/'.$player->name.'?size=50')->getContent();
$image = Image::make($image);
$this->assertEquals(50, $image->width());
$this->assertEquals(50, $image->height());
}
public function testAvatarByUser()
{
$disk = Storage::fake('textures');
$this->get('/avatar/user/0')
->assertSuccessful()
->assertHeader('Content-Type', 'image/png');
$user = factory(User::class)->create();
$this->get('/avatar/user/'.$user->uid)
->assertSuccessful()
->assertHeader('Content-Type', 'image/png');
$texture = factory(Texture::class)->create();
$disk->put($texture->hash, '');
$user->avatar = $texture->tid;
$user->save();
$image = $this->get('/avatar/user/'.$user->uid)
->assertSuccessful()
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($image);
$this->assertEquals(100, $image->width());
$this->assertEquals(100, $image->height());
$image = $this->get('/avatar/user/'.$user->uid.'?size=50')->getContent();
$image = Image::make($image);
$this->assertEquals(50, $image->width());
$this->assertEquals(50, $image->height());
}
public function testAvatarByTexture()
{
$disk = Storage::fake('textures');
$image = $this->get('/avatar/0')
->assertSuccessful()
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($image);
$this->assertEquals(100, $image->width());
$this->assertEquals(100, $image->height());
$texture = factory(Texture::class)->create();
$image = $this->get('/avatar/'.$texture->tid)
->assertSuccessful()
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($image);
$this->assertEquals(100, $image->width());
$this->assertEquals(100, $image->height());
$disk->put($texture->hash, '');
$image = $this->get('/avatar/'.$texture->tid)
->assertSuccessful()
->assertHeader('Content-Type', 'image/png')
->getContent();
$image = Image::make($image);
$this->assertEquals(100, $image->width());
$this->assertEquals(100, $image->height());
$this->assertTrue(Cache::has('avatar-2d-t'.$texture->tid.'-s100'));
$image = $this->get('/avatar/'.$texture->tid.'?size=50')->getContent();
$image = Image::make($image);
$this->assertEquals(50, $image->width());
$this->assertEquals(50, $image->height());
$this->assertTrue(Cache::has('avatar-2d-t'.$texture->tid.'-s50'));
}
}

View File

@ -11,12 +11,12 @@ class UserMenuComposerTest extends TestCase
$user = factory(User::class)->make();
$this->actingAs($user)
->get('/')
->assertSee(url('/avatar/user/'.$user->uid.'/25'));
->assertSee(url('/avatar/user/'.$user->uid.'?size=25'));
$this->actingAs($user)
->get('/skinlib')
->assertSee(url('/avatar/user/'.$user->uid.'/25'));
->assertSee(url('/avatar/user/'.$user->uid.'?size=25'));
$this->actingAs($user)
->get('/user')
->assertSee(url('/avatar/user/'.$user->uid.'/25'));
->assertSee(url('/avatar/user/'.$user->uid.'?size=25'));
}
}

View File

@ -12,7 +12,7 @@ class UserPanelComposerTest extends TestCase
$user = factory(User::class)->make();
$this->actingAs($user);
$this->get('/user')->assertSee(url('/avatar/user/'.$user->uid.'/45'));
$this->get('/user')->assertSee(url('/avatar/user/'.$user->uid.'?size=45'));
}
public function testBadges()

View File

@ -1,34 +0,0 @@
<?php
namespace Tests;
use App\Events\GetAvatarPreview;
use App\Models\Texture;
use Cache;
use Event;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Mockery;
use Storage;
class CacheAvatarPreviewTest extends TestCase
{
use DatabaseTransactions;
public function testHandle()
{
option(['enable_avatar_cache' => true]);
$provider = new \App\Providers\EventServiceProvider(app());
$provider->boot();
Storage::fake('textures');
$texture = factory(Texture::class)->create();
Storage::disk('textures')
->putFileAs('.', UploadedFile::fake()->image($texture->hash), $texture->hash);
$mock = Mockery::mock('overload:Minecraft');
$mock->shouldReceive('generateAvatarFromSkin')->andReturn(imagecreatetruecolor(1, 1));
event(new GetAvatarPreview($texture, 45));
$this->assertTrue(Cache::has("avatar-{$texture->tid}-45"));
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace Tests;
use App\Events\GetSkinPreview;
use App\Models\Texture;
use Cache;
use Event;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Mockery;
use Storage;
class CacheSkinPreviewTest extends TestCase
{
use DatabaseTransactions;
public function testHandle()
{
option(['enable_preview_cache' => true]);
$provider = new \App\Providers\EventServiceProvider(app());
$provider->boot();
Storage::fake('textures');
$skin = factory(Texture::class)->create();
Storage::disk('textures')
->putFileAs('.', UploadedFile::fake()->image($skin->hash), $skin->hash);
$mock = Mockery::mock('overload:Minecraft');
$mock->shouldReceive('generatePreviewFromSkin')->andReturn(imagecreatetruecolor(1, 1));
event(new GetSkinPreview($skin, 45));
$this->assertTrue(Cache::has("preview-{$skin->tid}-45"));
$cape = factory(Texture::class, 'cape')->create();
Storage::disk('textures')
->putFileAs('.', UploadedFile::fake()->image($cape->hash), $cape->hash);
$mock->shouldReceive('generatePreviewFromCape')->andReturn(imagecreatetruecolor(1, 1));
event(new GetSkinPreview($cape, 45));
$this->assertTrue(Cache::has("preview-{$cape->tid}-45"));
}
}

View File

@ -1,112 +0,0 @@
<?php
namespace Tests;
use App\Http\Controllers\TextureController;
use App\Services\Minecraft;
use Illuminate\Http\Testing\FileFactory;
class MinecraftTest extends TestCase
{
private $fileFactory;
protected function setUp(): void
{
parent::setUp();
$this->fileFactory = new FileFactory();
}
public function testGenerateAvatarFromSkin()
{
$file = $this->fileFactory->image('skin.png');
imagepng(imagecreatetruecolor(64, 32), $file->path());
$avatar = Minecraft::generateAvatarFromSkin(file_get_contents($file->path()), 50);
$this->assertEquals(50, imagesx($avatar));
$this->assertEquals(50, imagesy($avatar));
imagepng(imagecreatetruecolor(128, 64), $file->path());
$avatar = Minecraft::generateAvatarFromSkin(file_get_contents($file->path()), 50);
$this->assertEquals(50, imagesx($avatar));
$this->assertEquals(50, imagesy($avatar));
$avatar = Minecraft::generateAvatarFromSkin(
base64_decode(TextureController::getDefaultSteveSkin()), 50, 'l'
);
$this->assertEquals(50, imagesx($avatar));
$this->assertEquals(50, imagesy($avatar));
}
public function testGeneratePreviewFromSkin()
{
$file = $this->fileFactory->image('skin.png');
imagepng(imagecreatetruecolor(64, 32), $file->path());
$preview = Minecraft::generatePreviewFromSkin(
file_get_contents($file->path()), 50, false, 'front'
);
$this->assertEquals(25, imagesx($preview));
$this->assertEquals(50, imagesy($preview));
imagepng(imagecreatetruecolor(64, 32), $file->path());
$preview = Minecraft::generatePreviewFromSkin(
file_get_contents($file->path()),
50,
true, // Alex model
'both',
4
);
$this->assertEquals(56, imagesx($preview));
$this->assertEquals(50, imagesy($preview));
imagepng(imagecreatetruecolor(64, 64), $file->path());
$preview = Minecraft::generatePreviewFromSkin(
file_get_contents($file->path()),
100,
true, // Alex model
'both',
8
);
$this->assertEquals(125, imagesx($preview));
$this->assertEquals(100, imagesy($preview));
imagepng(imagecreatetruecolor(128, 64), $file->path());
$preview = Minecraft::generatePreviewFromSkin(file_get_contents($file->path()), 50);
$this->assertEquals(56, imagesx($preview));
$this->assertEquals(50, imagesy($preview));
imagepng(imagecreatetruecolor(128, 128), $file->path());
$preview = Minecraft::generatePreviewFromSkin(file_get_contents($file->path()), 50);
$this->assertEquals(56, imagesx($preview));
$this->assertEquals(50, imagesy($preview));
$preview = Minecraft::generatePreviewFromSkin(
base64_decode(TextureController::getDefaultSteveSkin()),
50,
false,
'back'
);
$this->assertEquals(25, imagesx($preview));
$this->assertEquals(50, imagesy($preview));
}
public function testGeneratePreviewFromCape()
{
$file = $this->fileFactory->image('cape.png');
imagepng(imagecreatetruecolor(128, 64), $file->path());
$preview = Minecraft::generatePreviewFromCape(file_get_contents($file->path()), 64);
$this->assertEquals(40, imagesx($preview));
$this->assertEquals(64, imagesy($preview));
imagepng(imagecreatetruecolor(128, 64), $file->path());
$preview = Minecraft::generatePreviewFromCape(
file_get_contents($file->path()),
64,
281,
250
);
$this->assertEquals(281, imagesx($preview));
$this->assertEquals(250, imagesy($preview));
}
}