更改Imagick数据处理

把Imagick获取到的GD数据转为二进制交给Imagick处理
This commit is contained in:
SANYE-YA 2025-08-07 05:17:15 +08:00 committed by GitHub
parent 33055ecbf9
commit 77a58bbe4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -2,460 +2,191 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Player;
use App\Models\Texture; use App\Models\Texture;
use App\Models\User; use App\Models\User;
use Blessing\Filter; use Blessing\Minecraft;
use Blessing\Rejection; use Carbon\Carbon;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Intervention\Image\Facades\Image; use Intervention\Image\Facades\Image;
use League\CommonMark\GithubFlavoredMarkdownConverter;
class SkinlibController extends Controller class TextureController extends Controller
{ {
public function __construct() public function __construct()
{ {
$this->middleware(function (Request $request, $next) { $this->middleware('cache.headers:public;max_age='.option('cache_expire_time'))
/** @var User */ ->only(['json']);
$user = $request->user();
/** @var Texture */
$texture = $request->route('texture');
if ($texture->uploader != $user->uid && !$user->isAdmin()) { $this->middleware('cache.headers:etag;public;max_age='.option('cache_expire_time'))
return json(trans('skinlib.no-permission'), 1) ->only([
->setStatusCode(403); 'preview',
} 'raw',
'texture',
return $next($request); 'avatarByPlayer',
})->only(['rename', 'privacy', 'type', 'delete']); 'avatarByUser',
'avatarByTexture',
$this->middleware(function (Request $request, $next) { ]);
/** @var User */
$user = $request->user();
/** @var Texture */
$texture = $request->route('texture');
if (!$texture->public) {
if (!Auth::check() || ($user->uid != $texture->uploader && !$user->isAdmin())) {
$statusCode = (int) option('status_code_for_private');
if ($statusCode === 404) {
abort($statusCode, trans('skinlib.show.deleted'));
} else {
abort(403, trans('skinlib.show.private'));
}
}
}
return $next($request);
})->only(['show', 'info']);
} }
public function library(Request $request) public function json($player)
{ {
$user = Auth::user(); $player = Player::where('name', $player)->firstOrFail();
$isBanned = $player->user->permission === User::BANNED;
abort_if($isBanned, 403, trans('general.player-banned'));
// Available filters: skin, steve, alex, cape return response()->json($player)->setLastModified($player->last_modified);
$type = $request->input('filter', 'skin');
$uploader = $request->input('uploader');
$keyword = $request->input('keyword');
$sort = $request->input('sort', 'time');
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
return Texture::orderBy($sortBy, 'desc')
->when(
$type === 'skin',
fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
fn (Builder $query) => $query->where('type', $type),
)
->when($keyword, fn (Builder $query, $keyword) => $query->like('name', $keyword))
->when($uploader, fn (Builder $query, $uploader) => $query->where('uploader', $uploader))
->when($user, function (Builder $query, User $user) {
if (!$user->isAdmin()) {
// use closure-style `where` clause to lift up SQL priority
return $query->where(function (Builder $query) use ($user) {
$query
->where('public', true)
->orWhere('uploader', $user->uid);
});
}
}, function (Builder $query) {
// show public textures only to anonymous visitors
return $query->where('public', true);
})
->join('users', 'uid', 'uploader')
->select(['tid', 'name', 'type', 'uploader', 'public', 'likes', 'nickname'])
->paginate(20);
} }
public function show(Filter $filter, Texture $texture) public function previewByHash(Minecraft $minecraft, Request $request, $hash)
{
$texture = Texture::where('hash', $hash)->firstOrFail();
return $this->preview($minecraft, $request, $texture);
}
public function preview(Minecraft $minecraft, Request $request, Texture $texture)
{
$tid = $texture->tid;
$hash = $texture->hash;
$usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
$format = $usePNG ? 'png' : 'webp';
$disk = Storage::disk('textures');
abort_if($disk->missing($hash), 404);
$height = (int) $request->query('height', 200);
$now = Carbon::now();
$response = Cache::remember(
'preview-t'.$tid."-$format",
option('enable_preview_cache') ? $now->addYear() : $now->addMinute(),
function () use ($minecraft, $disk, $texture, $hash, $height, $usePNG) {
$file = $disk->get($hash);
if ($texture->type === 'cape') {
$gdResource = $minecraft->renderCape($file, $height);
} else {
$gdResource = $minecraft->renderSkin($file, 12, $texture->type === 'alex');
}
ob_start();
imagepng($gdResource);
$imageBlob = ob_get_clean();
imagedestroy($gdResource);
$imagick = new \Imagick();
$imagick->readImageBlob($imageBlob);
if (!$usePNG) {
$imagick->setImageFormat('webp');
}
$response = response($imagick->getImageBlob(), 200, [
'Content-Type' => $usePNG ? 'image/png' : 'image/webp',
'Content-Length' => strlen($imagick->getImageBlob()),
]);
$response->setLastModified(Carbon::createFromTimestamp($disk->lastModified($hash)));
return $response;
}
);
return $response;
}
public function raw($tid)
{
abort_unless(option('allow_downloading_texture'), 403);
$texture = Texture::findOrFail($tid);
return $this->texture($texture->hash);
}
public function texture(string $hash)
{ {
/** @var User */
$user = Auth::user();
/** @var FilesystemAdapter */
$disk = Storage::disk('textures'); $disk = Storage::disk('textures');
abort_if($disk->missing($hash), 404);
if ($disk->missing($texture->hash)) { $lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
if (option('auto_del_invalid_texture')) {
$texture->delete();
}
abort(404, trans('skinlib.show.deleted'));
}
$badges = []; return response($disk->get($hash))
$uploader = $texture->owner; ->withHeaders([
if ($uploader) { 'Content-Type' => 'image/png',
if ($uploader->isAdmin()) { 'Content-Length' => $disk->size($hash),
$badges[] = ['text' => 'STAFF', 'color' => 'primary']; ])
} ->setLastModified($lastModified);
$badges = $filter->apply('user_badges', $badges, [$uploader]);
}
$grid = [
'layout' => [
['md-8', 'md-4'],
],
'widgets' => [
[
['shared.previewer'],
['skinlib.widgets.show.side'],
],
],
];
$grid = $filter->apply('grid:skinlib.show', $grid);
return view('skinlib.show')
->with('texture', $texture)
->with('grid', $grid)
->with('extra', [
'download' => (bool) option('allow_downloading_texture'),
'currentUid' => $user ? $user->uid : 0,
'admin' => $user && $user->isAdmin(),
'inCloset' => $user && $user->closet()->where('tid', $texture->tid)->count() > 0,
'uploaderExists' => (bool) $uploader,
'nickname' => optional($uploader)->nickname ?? trans('general.unexistent-user'),
'report' => intval(option('reporter_score_modification', 0)),
'badges' => $badges,
]);
} }
public function info(Texture $texture) public function avatarByPlayer(Minecraft $minecraft, Request $request, $name)
{ {
return $texture; $player = Player::where('name', $name)->firstOrFail();
return $this->avatar($minecraft, $request, $player->skin);
} }
public function upload(Filter $filter) public function avatarByUser(Minecraft $minecraft, Request $request, $uid)
{ {
$grid = [ $texture = Texture::find(optional(User::find($uid))->avatar);
'layout' => [
['md-6', 'md-6'],
],
'widgets' => [
[
['skinlib.widgets.upload.input'],
['shared.previewer'],
],
],
];
$grid = $filter->apply('grid:skinlib.upload', $grid);
$converter = new GithubFlavoredMarkdownConverter(); return $this->avatar($minecraft, $request, $texture);
return view('skinlib.upload')
->with('grid', $grid)
->with('extra', [
'rule' => ($regexp = option('texture_name_regexp'))
? trans('skinlib.upload.name-rule-regexp', compact('regexp'))
: trans('skinlib.upload.name-rule'),
'privacyNotice' => trans(
'skinlib.upload.private-score-notice',
['score' => option('private_score_per_storage')]
),
'score' => (int) auth()->user()->score,
'scorePublic' => (int) option('score_per_storage'),
'scorePrivate' => (int) option('private_score_per_storage'),
'closetItemCost' => (int) option('score_per_closet_item'),
'award' => (int) option('score_award_per_texture'),
'contentPolicy' => $converter->convertToHtml(option_localized('content_policy'))->getContent(),
]);
} }
public function handleUpload( public function avatarByHash(Minecraft $minecraft, Request $request, $hash)
Request $request, {
Filter $filter, $texture = Texture::where('hash', $hash)->first();
Dispatcher $dispatcher,
) { return $this->avatar($minecraft, $request, $texture);
$file = $request->file('file'); }
if ($file && !$file->isValid()) {
Log::error($file->getErrorMessage()); public function avatarByTexture(Minecraft $minecraft, Request $request, $tid)
{
$texture = Texture::find($tid);
return $this->avatar($minecraft, $request, $texture);
}
protected function avatar(Minecraft $minecraft, Request $request, ?Texture $texture)
{
if (!empty($texture) && $texture->type !== 'steve' && $texture->type !== 'alex') {
return abort(422);
} }
$data = $request->validate([ $size = (int) $request->query('size', 100);
'name' => [ $mode = $request->has('3d') ? '3d' : '2d';
'required', $usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
option('texture_name_regexp') ? 'regex:'.option('texture_name_regexp') : 'string', $format = $usePNG ? 'png' : 'webp';
],
'file' => 'required|mimes:png|max:'.option('max_upload_file_size'),
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
'public' => 'required|boolean',
]);
/** @var UploadedFile */ $disk = Storage::disk('textures');
$file = $filter->apply('uploaded_texture_file', $file); if (is_null($texture) || $disk->missing($texture->hash)) {
// TODO: refactor
return Image::configure(['driver' => 'imagick'])->make(resource_path("misc/textures/avatar$mode.png"))
->resize($size, $size)
->response($usePNG ? 'png' : 'webp', 100);
}
$name = $data['name']; $hash = $texture->hash;
$name = $filter->apply('uploaded_texture_name', $name, [$file]); $now = Carbon::now();
$response = Cache::remember(
'avatar-'.$mode.'-t'.$texture->tid.'-s'.$size."-$format",
option('enable_avatar_cache') ? $now->addYear() : $now->addMinute(),
function () use ($minecraft, $disk, $hash, $size, $mode, $usePNG) {
$file = $disk->get($hash);
if ($mode === '3d') {
$image = $minecraft->render3dAvatar($file, 25);
} else {
$image = $minecraft->render2dAvatar($file, 25);
}
$can = $filter->apply('can_upload_texture', true, [$file, $name]); $lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$type = $data['type']; return \Intervention\Image\ImageManagerStatic::configure(['driver' => 'gd'])->make($image)
$size = getimagesize($file); ->resize($size, $size)
->response($usePNG ? 'png' : 'webp', 100)
$maxWidth = option('max_texture_width', 8192); ->setLastModified($lastModified);
if ($size[0] > $maxWidth) {
$message = trans('skinlib.upload.too-wide', [
'width' => $size[0],
'maxWidth' => $maxWidth,
]);
return json($message, 1);
}
if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
$message = trans('skinlib.upload.invalid-size', [
'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'),
'width' => $size[0],
'height' => $size[1],
]);
return json($message, 1);
}
$ratio = $size[0] / $size[1];
if ($type == 'steve' || $type == 'alex') {
if ($ratio != 2 && $ratio != 1 || $type === 'alex' && $ratio === 2) {
$message = trans('skinlib.upload.invalid-size', [
'type' => trans('general.skin'),
'width' => $size[0],
'height' => $size[1],
]);
return json($message, 1);
} }
} elseif ($type == 'cape') {
if ($ratio != 2) {
$message = trans('skinlib.upload.invalid-size', [
'type' => trans('general.cape'),
'width' => $size[0],
'height' => $size[1],
]);
return json($message, 1);
}
}
$image = Image::make($file);
$imagick = $image->getCore();
$imagick->setOption('png:compression-filter', '0');
$imagick->setOption('png:compression-level', '9');
$imagick->setOption('png:compression-strategy', '0');
$imagick->setOption('png:exclude-chunk', 'all');
$imagick->stripImage();
$sanitized = $image->encode('png')->getEncoded();
$hash = hash('sha256', $image->encoded);
$hash = $filter->apply('uploaded_texture_hash', $hash, [$image]);
/** @var User */
$user = Auth::user();
$duplicated = Texture::where('hash', $hash)
->where(
fn (Builder $query) => $query->where('public', true)->orWhere('uploader', $user->uid)
)
->first();
if ($duplicated) {
// if the texture already uploaded was set to private,
// then allow to re-upload it.
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
}
$fileSize = ceil(strlen($sanitized) / 1024);
$isPublic = is_string($data['public'])
? $data['public'] === '1'
: $data['public'];
$cost = $fileSize * (
$isPublic
? option('score_per_storage')
: option('private_score_per_storage')
); );
$cost += option('score_per_closet_item');
$cost -= option('score_award_per_texture', 0);
if ($user->score < $cost) {
return json(trans('skinlib.upload.lack-score'), 1);
}
$dispatcher->dispatch('texture.uploading', [$image, $name, $hash]); return $response;
$texture = new Texture();
$texture->name = $name;
$texture->type = $type;
$texture->hash = $hash;
$texture->size = $fileSize;
$texture->public = $isPublic;
$texture->uploader = $user->uid;
$texture->likes = 1;
$texture->save();
/** @var FilesystemAdapter */
$disk = Storage::disk('textures');
if ($disk->missing($hash)) {
$disk->put($hash, $sanitized);
}
$user->score -= $cost;
$user->closet()->attach($texture->tid, ['item_name' => $name]);
$user->save();
$dispatcher->dispatch('texture.uploaded', [$texture, $image]);
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
'tid' => $texture->tid,
]);
}
public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter)
{
$can = $filter->apply('can_delete_texture', true, [$texture]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$dispatcher->dispatch('texture.deleting', [$texture]);
// check if file occupied
if (Texture::where('hash', $texture->hash)->count() === 1) {
Storage::disk('textures')->delete($texture->hash);
}
$texture->delete();
$dispatcher->dispatch('texture.deleted', [$texture]);
return json(trans('skinlib.delete.success'), 0);
}
public function privacy(Texture $texture, Dispatcher $dispatcher, Filter $filter)
{
$can = $filter->apply('can_update_texture_privacy', true, [$texture]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$uploader = $texture->owner;
$score_diff = $texture->size
* (option('private_score_per_storage') - option('score_per_storage'))
* ($texture->public ? -1 : 1);
if ($texture->public && option('take_back_scores_after_deletion', true)) {
$score_diff -= option('score_award_per_texture', 0);
}
if ($uploader->score + $score_diff < 0) {
return json(trans('skinlib.upload.lack-score'), 1);
}
if (!$texture->public) {
$duplicated = Texture::where('hash', $texture->hash)
->where('public', true)
->first();
if ($duplicated) {
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
}
}
$dispatcher->dispatch('texture.privacy.updating', [$texture]);
$uploader->score += $score_diff;
$uploader->save();
$texture->public = !$texture->public;
$texture->save();
$dispatcher->dispatch('texture.privacy.updated', [$texture]);
$message = trans('skinlib.privacy.success', [
'privacy' => (
$texture->public
? trans('general.public')
: trans('general.private')),
]);
return json($message, 0);
}
public function rename(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
Texture $texture,
) {
$data = $request->validate(['name' => [
'required',
option('texture_name_regexp')
? 'regex:'.option('texture_name_regexp')
: 'string',
]]);
$name = $data['name'];
$can = $filter->apply('can_update_texture_name', true, [$texture, $name]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$dispatcher->dispatch('texture.name.updating', [$texture, $name]);
$old = $texture->replicate();
$texture->name = $name;
$texture->save();
$dispatcher->dispatch('texture.name.updated', [$texture, $old]);
return json(trans('skinlib.rename.success', ['name' => $name]), 0);
}
public function type(
Request $request,
Dispatcher $dispatcher,
Filter $filter,
Texture $texture,
) {
$data = $request->validate([
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
]);
$type = $data['type'];
$can = $filter->apply('can_update_texture_type', true, [$texture, $type]);
if ($can instanceof Rejection) {
return json($can->getReason(), 1);
}
$dispatcher->dispatch('texture.type.updating', [$texture, $type]);
$old = $texture->replicate();
$texture->type = $type;
$texture->save();
$dispatcher->dispatch('texture.type.updated', [$texture, $old]);
return json(trans('skinlib.model.success', ['model' => $type]), 0);
} }
} }