switch server-side texture renderer
This commit is contained in:
parent
6c221c28d6
commit
b1ccdb47f2
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
39
composer.lock
generated
|
|
@ -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"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
26
tests/Fakes/Minecraft.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user