From b1ccdb47f2162c58a7561b1669d9a8011f35d1ef Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Sun, 12 Jan 2020 09:27:39 +0800 Subject: [PATCH] switch server-side texture renderer --- app/Events/GetAvatarPreview.php | 25 -- app/Events/GetSkinPreview.php | 25 -- app/Http/Controllers/TextureController.php | 232 +++++++-------- app/Http/Controllers/UpdateController.php | 2 + app/Http/Kernel.php | 1 + app/Http/View/Composers/UserMenuComposer.php | 2 +- app/Http/View/Composers/UserPanelComposer.php | 2 +- app/Listeners/CacheAvatarPreview.php | 25 -- app/Listeners/CacheSkinPreview.php | 34 --- app/Providers/EventServiceProvider.php | 6 - app/Services/Minecraft.php | 218 -------------- app/helpers.php | 13 - composer.json | 1 + composer.lock | 39 ++- .../src/components/ApplyToPlayerDialog.vue | 2 +- .../assets/src/components/ClosetItem.vue | 2 +- .../assets/src/components/SkinLibItem.vue | 4 +- resources/assets/src/views/admin/Players.vue | 4 +- resources/assets/src/views/skinlib/Show.vue | 2 +- resources/assets/src/views/user/Players.vue | 4 +- .../components/ApplyToPlayerDialog.test.ts | 2 +- .../tests/components/ClosetItem.test.ts | 2 +- .../tests/components/SkinLibItem.test.ts | 2 +- .../assets/tests/views/admin/Players.test.ts | 2 +- .../assets/tests/views/user/Closet.test.ts | 2 +- .../assets/tests/views/user/Players.test.ts | 4 +- routes/static.php | 10 +- tests/Fakes/Minecraft.php | 26 ++ .../ControllersTest/TextureControllerTest.php | 268 +++++++++--------- .../ComposersTest/UserMenuComposerTest.php | 6 +- .../ComposersTest/UserPanelComposerTest.php | 2 +- .../ListenersTest/CacheAvatarPreviewTest.php | 34 --- tests/ListenersTest/CacheSkinPreviewTest.php | 42 --- tests/ServicesTest/MinecraftTest.php | 112 -------- 34 files changed, 328 insertions(+), 829 deletions(-) delete mode 100644 app/Events/GetAvatarPreview.php delete mode 100644 app/Events/GetSkinPreview.php delete mode 100644 app/Listeners/CacheAvatarPreview.php delete mode 100644 app/Listeners/CacheSkinPreview.php delete mode 100644 app/Services/Minecraft.php create mode 100644 tests/Fakes/Minecraft.php delete mode 100644 tests/ListenersTest/CacheAvatarPreviewTest.php delete mode 100644 tests/ListenersTest/CacheSkinPreviewTest.php delete mode 100644 tests/ServicesTest/MinecraftTest.php diff --git a/app/Events/GetAvatarPreview.php b/app/Events/GetAvatarPreview.php deleted file mode 100644 index d2a64716..00000000 --- a/app/Events/GetAvatarPreview.php +++ /dev/null @@ -1,25 +0,0 @@ -texture = $texture; - $this->size = $size; - } -} diff --git a/app/Events/GetSkinPreview.php b/app/Events/GetSkinPreview.php deleted file mode 100644 index bae7c23c..00000000 --- a/app/Events/GetSkinPreview.php +++ /dev/null @@ -1,25 +0,0 @@ -texture = $texture; - $this->size = $size; - } -} diff --git a/app/Http/Controllers/TextureController.php b/app/Http/Controllers/TextureController.php index 2996a8df..c0074e52 100644 --- a/app/Http/Controllers/TextureController.php +++ b/app/Http/Controllers/TextureController.php @@ -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'; - } } diff --git a/app/Http/Controllers/UpdateController.php b/app/Http/Controllers/UpdateController.php index e6d1ac50..955700c1 100644 --- a/app/Http/Controllers/UpdateController.php +++ b/app/Http/Controllers/UpdateController.php @@ -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'); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f7e8cfa3..03063887 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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, diff --git a/app/Http/View/Composers/UserMenuComposer.php b/app/Http/View/Composers/UserMenuComposer.php index 68a51bd2..dbfd1f2c 100644 --- a/app/Http/View/Composers/UserMenuComposer.php +++ b/app/Http/View/Composers/UserMenuComposer.php @@ -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')); diff --git a/app/Http/View/Composers/UserPanelComposer.php b/app/Http/View/Composers/UserPanelComposer.php index 7b8c422a..2d201b0b 100644 --- a/app/Http/View/Composers/UserPanelComposer.php +++ b/app/Http/View/Composers/UserPanelComposer.php @@ -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 = []; diff --git a/app/Listeners/CacheAvatarPreview.php b/app/Listeners/CacheAvatarPreview.php deleted file mode 100644 index aeec92c3..00000000 --- a/app/Listeners/CacheAvatarPreview.php +++ /dev/null @@ -1,25 +0,0 @@ -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']); - } -} diff --git a/app/Listeners/CacheSkinPreview.php b/app/Listeners/CacheSkinPreview.php deleted file mode 100644 index 5c34943b..00000000 --- a/app/Listeners/CacheSkinPreview.php +++ /dev/null @@ -1,34 +0,0 @@ -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), - ]); - } -} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 4a489212..83a12a0a 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -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); } diff --git a/app/Services/Minecraft.php b/app/Services/Minecraft.php deleted file mode 100644 index 51b91fed..00000000 --- a/app/Services/Minecraft.php +++ /dev/null @@ -1,218 +0,0 @@ -. - * - * @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); - } -} diff --git a/app/helpers.php b/app/helpers.php index 0068690e..7b9f70bc 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -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; - } -} diff --git a/composer.json b/composer.json index 0b6480da..f48d6d04 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 56c8760b..a70ccc5a 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } ], diff --git a/resources/assets/src/components/ApplyToPlayerDialog.vue b/resources/assets/src/components/ApplyToPlayerDialog.vue index 20c23cc8..7e926608 100644 --- a/resources/assets/src/components/ApplyToPlayerDialog.vue +++ b/resources/assets/src/components/ApplyToPlayerDialog.vue @@ -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` }, }, } diff --git a/resources/assets/src/components/ClosetItem.vue b/resources/assets/src/components/ClosetItem.vue index d0e52c80..0bb1cb8c 100644 --- a/resources/assets/src/components/ClosetItem.vue +++ b/resources/assets/src/components/ClosetItem.vue @@ -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}` diff --git a/resources/assets/src/components/SkinLibItem.vue b/resources/assets/src/components/SkinLibItem.vue index 601ea1c5..7c428717 100644 --- a/resources/assets/src/components/SkinLibItem.vue +++ b/resources/assets/src/components/SkinLibItem.vue @@ -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 diff --git a/resources/assets/src/views/admin/Players.vue b/resources/assets/src/views/admin/Players.vue index 82e56f61..48f4b8c1 100644 --- a/resources/assets/src/views/admin/Players.vue +++ b/resources/assets/src/views/admin/Players.vue @@ -36,13 +36,13 @@ v-if="props.row.tid_skin" :href="`${baseUrl}/skinlib/show/${props.row.tid_skin}`" > - + - + diff --git a/resources/assets/src/views/skinlib/Show.vue b/resources/assets/src/views/skinlib/Show.vue index d6bb0a8c..89d8daca 100644 --- a/resources/assets/src/views/skinlib/Show.vue +++ b/resources/assets/src/views/skinlib/Show.vue @@ -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) diff --git a/resources/assets/src/views/user/Players.vue b/resources/assets/src/views/user/Players.vue index 54551957..3967b112 100644 --- a/resources/assets/src/views/user/Players.vue +++ b/resources/assets/src/views/user/Players.vue @@ -75,7 +75,7 @@ @@ -86,7 +86,7 @@ diff --git a/resources/assets/tests/components/ApplyToPlayerDialog.test.ts b/resources/assets/tests/components/ApplyToPlayerDialog.test.ts index 0ba2b8a5..55612143 100644 --- a/resources/assets/tests/components/ApplyToPlayerDialog.test.ts +++ b/resources/assets/tests/components/ApplyToPlayerDialog.test.ts @@ -48,5 +48,5 @@ test('compute avatar URL', () => { // eslint-disable-next-line camelcase const wrapper = mount(ApplyToPlayerDialog) const { avatarUrl } = wrapper.vm - expect(avatarUrl({ tid_skin: 1 })).toBe('/avatar/1/35') + expect(avatarUrl({ tid_skin: 1 })).toBe('/avatar/1?size=35') }) diff --git a/resources/assets/tests/components/ClosetItem.test.ts b/resources/assets/tests/components/ClosetItem.test.ts index d2181573..ccd50dfd 100644 --- a/resources/assets/tests/components/ClosetItem.test.ts +++ b/resources/assets/tests/components/ClosetItem.test.ts @@ -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') diff --git a/resources/assets/tests/components/SkinLibItem.test.ts b/resources/assets/tests/components/SkinLibItem.test.ts index fedc2357..fc78fd6c 100644 --- a/resources/assets/tests/components/SkinLibItem.test.ts +++ b/resources/assets/tests/components/SkinLibItem.test.ts @@ -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', () => { diff --git a/resources/assets/tests/views/admin/Players.test.ts b/resources/assets/tests/views/admin/Players.test.ts index 13f5580f..8d923641 100644 --- a/resources/assets/tests/views/admin/Players.test.ts +++ b/resources/assets/tests/views/admin/Players.test.ts @@ -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 () => { diff --git a/resources/assets/tests/views/user/Closet.test.ts b/resources/assets/tests/views/user/Closet.test.ts index 3630a1e3..8d854923 100644 --- a/resources/assets/tests/views/user/Closet.test.ts +++ b/resources/assets/tests/views/user/Closet.test.ts @@ -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() }) diff --git a/resources/assets/tests/views/user/Players.test.ts b/resources/assets/tests/views/user/Players.test.ts index 4d76180b..377afdc1 100644 --- a/resources/assets/tests/views/user/Players.test.ts +++ b/resources/assets/tests/views/user/Players.test.ts @@ -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 () => { diff --git a/routes/static.php b/routes/static.php index 5e7c1851..861cd323 100644 --- a/routes/static.php +++ b/routes/static.php @@ -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'); diff --git a/tests/Fakes/Minecraft.php b/tests/Fakes/Minecraft.php new file mode 100644 index 00000000..3543f4ce --- /dev/null +++ b/tests/Fakes/Minecraft.php @@ -0,0 +1,26 @@ +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')); } } diff --git a/tests/HttpTest/ViewTest/ComposersTest/UserMenuComposerTest.php b/tests/HttpTest/ViewTest/ComposersTest/UserMenuComposerTest.php index d2c29d8b..d819ad06 100644 --- a/tests/HttpTest/ViewTest/ComposersTest/UserMenuComposerTest.php +++ b/tests/HttpTest/ViewTest/ComposersTest/UserMenuComposerTest.php @@ -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')); } } diff --git a/tests/HttpTest/ViewTest/ComposersTest/UserPanelComposerTest.php b/tests/HttpTest/ViewTest/ComposersTest/UserPanelComposerTest.php index c9e04daa..38ad06d6 100644 --- a/tests/HttpTest/ViewTest/ComposersTest/UserPanelComposerTest.php +++ b/tests/HttpTest/ViewTest/ComposersTest/UserPanelComposerTest.php @@ -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() diff --git a/tests/ListenersTest/CacheAvatarPreviewTest.php b/tests/ListenersTest/CacheAvatarPreviewTest.php deleted file mode 100644 index f7293e86..00000000 --- a/tests/ListenersTest/CacheAvatarPreviewTest.php +++ /dev/null @@ -1,34 +0,0 @@ - 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")); - } -} diff --git a/tests/ListenersTest/CacheSkinPreviewTest.php b/tests/ListenersTest/CacheSkinPreviewTest.php deleted file mode 100644 index 8c499a21..00000000 --- a/tests/ListenersTest/CacheSkinPreviewTest.php +++ /dev/null @@ -1,42 +0,0 @@ - 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")); - } -} diff --git a/tests/ServicesTest/MinecraftTest.php b/tests/ServicesTest/MinecraftTest.php deleted file mode 100644 index 92ffdc92..00000000 --- a/tests/ServicesTest/MinecraftTest.php +++ /dev/null @@ -1,112 +0,0 @@ -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)); - } -}