193 lines
6.2 KiB
PHP
193 lines
6.2 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Support\MarkdownRenderer;
|
||
use GdImage;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Support\Facades\Storage;
|
||
use Illuminate\Support\Str;
|
||
use RuntimeException;
|
||
use Throwable;
|
||
|
||
class UploadController extends Controller
|
||
{
|
||
private const MAX_FILE_KB = 6144;
|
||
|
||
private const MAX_MAIN_WIDTH = 2400;
|
||
|
||
private const THUMB_WIDTH = 320;
|
||
|
||
private const MAIN_WEBP_QUALITY = 86;
|
||
|
||
private const THUMB_WEBP_QUALITY = 80;
|
||
|
||
public function markdownPreview(Request $request, MarkdownRenderer $markdownRenderer): JsonResponse
|
||
{
|
||
$validated = $request->validate([
|
||
'markdown' => ['nullable', 'string'],
|
||
]);
|
||
|
||
return response()->json([
|
||
'success' => true,
|
||
'html' => $markdownRenderer->render((string) ($validated['markdown'] ?? '')),
|
||
]);
|
||
}
|
||
|
||
public function markdownImage(Request $request): JsonResponse
|
||
{
|
||
$validated = $request->validate([
|
||
'image' => ['required', 'file', 'image', 'mimes:jpg,jpeg,png,webp,gif', 'max:' . self::MAX_FILE_KB],
|
||
], [
|
||
'image.required' => '请选择要上传的图片。',
|
||
'image.image' => '仅支持图片文件上传。',
|
||
'image.mimes' => '图片格式仅支持 jpg、jpeg、png、webp、gif。',
|
||
'image.max' => '图片大小不能超过 6MB。',
|
||
]);
|
||
|
||
if (! function_exists('imagecreatefromstring') || ! function_exists('imagewebp')) {
|
||
return response()->json([
|
||
'success' => false,
|
||
'message' => '服务器不支持图片处理能力(GD/WebP)。',
|
||
], 500);
|
||
}
|
||
|
||
/** @var UploadedFile $file */
|
||
$file = $validated['image'];
|
||
|
||
$sourceImage = null;
|
||
$mainImage = null;
|
||
$thumbImage = null;
|
||
|
||
try {
|
||
[$sourceImage, $sourceWidth, $sourceHeight] = $this->createImageFromUpload($file);
|
||
|
||
$mainWidth = min(self::MAX_MAIN_WIDTH, $sourceWidth);
|
||
$mainImage = $this->resizeImage($sourceImage, $sourceWidth, $sourceHeight, $mainWidth);
|
||
|
||
$thumbWidth = min(self::THUMB_WIDTH, imagesx($mainImage));
|
||
$thumbImage = $this->resizeImage($mainImage, imagesx($mainImage), imagesy($mainImage), $thumbWidth);
|
||
|
||
$mainBinary = $this->encodeWebp($mainImage, self::MAIN_WEBP_QUALITY);
|
||
$thumbBinary = $this->encodeWebp($thumbImage, self::THUMB_WEBP_QUALITY);
|
||
|
||
$subDir = now()->format('Y/m/d');
|
||
$baseName = now()->format('YmdHis') . '-' . Str::lower(Str::random(8));
|
||
$mainPath = "markdown-images/{$subDir}/{$baseName}.webp";
|
||
$thumbPath = "markdown-images/{$subDir}/{$baseName}_thumb.webp";
|
||
|
||
Storage::disk('public')->put($mainPath, $mainBinary);
|
||
Storage::disk('public')->put($thumbPath, $thumbBinary);
|
||
|
||
$mainUrl = '/storage/' . ltrim($mainPath, '/');
|
||
$thumbUrl = '/storage/' . ltrim($thumbPath, '/');
|
||
|
||
return response()->json([
|
||
'success' => true,
|
||
'url' => $mainUrl,
|
||
'thumb_url' => $thumbUrl,
|
||
'filename' => basename($mainPath),
|
||
'markdown' => '',
|
||
]);
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return response()->json([
|
||
'success' => false,
|
||
'message' => '图片上传失败,请稍后重试。',
|
||
], 422);
|
||
} finally {
|
||
if ($sourceImage instanceof GdImage) {
|
||
imagedestroy($sourceImage);
|
||
}
|
||
|
||
if ($mainImage instanceof GdImage) {
|
||
imagedestroy($mainImage);
|
||
}
|
||
|
||
if ($thumbImage instanceof GdImage) {
|
||
imagedestroy($thumbImage);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @return array{0:GdImage,1:int,2:int}
|
||
*/
|
||
private function createImageFromUpload(UploadedFile $file): array
|
||
{
|
||
$path = $file->getRealPath();
|
||
if ($path === false || $path === '') {
|
||
throw new RuntimeException('上传临时文件不可用。');
|
||
}
|
||
|
||
$binary = file_get_contents($path);
|
||
if ($binary === false) {
|
||
throw new RuntimeException('读取上传文件失败。');
|
||
}
|
||
|
||
$image = @imagecreatefromstring($binary);
|
||
if (! $image instanceof GdImage) {
|
||
throw new RuntimeException('无法解析图片内容。');
|
||
}
|
||
|
||
if (! imageistruecolor($image)) {
|
||
imagepalettetotruecolor($image);
|
||
}
|
||
|
||
$width = imagesx($image);
|
||
$height = imagesy($image);
|
||
|
||
if ($width < 1 || $height < 1) {
|
||
imagedestroy($image);
|
||
throw new RuntimeException('图片尺寸无效。');
|
||
}
|
||
|
||
return [$image, $width, $height];
|
||
}
|
||
|
||
private function resizeImage(GdImage $source, int $sourceWidth, int $sourceHeight, int $targetWidth): GdImage
|
||
{
|
||
$targetWidth = max(1, $targetWidth);
|
||
|
||
if ($sourceWidth === $targetWidth) {
|
||
$targetHeight = $sourceHeight;
|
||
} else {
|
||
$targetHeight = max(1, (int) round($sourceHeight * ($targetWidth / $sourceWidth)));
|
||
}
|
||
|
||
$canvas = imagecreatetruecolor($targetWidth, $targetHeight);
|
||
if (! $canvas instanceof GdImage) {
|
||
throw new RuntimeException('创建图片画布失败。');
|
||
}
|
||
|
||
$background = imagecolorallocate($canvas, 255, 255, 255);
|
||
imagefill($canvas, 0, 0, $background);
|
||
|
||
if (! imagecopyresampled($canvas, $source, 0, 0, 0, 0, $targetWidth, $targetHeight, $sourceWidth, $sourceHeight)) {
|
||
imagedestroy($canvas);
|
||
throw new RuntimeException('图片缩放失败。');
|
||
}
|
||
|
||
return $canvas;
|
||
}
|
||
|
||
private function encodeWebp(GdImage $image, int $quality): string
|
||
{
|
||
ob_start();
|
||
$saved = imagewebp($image, null, $quality);
|
||
$binary = ob_get_clean();
|
||
|
||
if (! $saved || ! is_string($binary) || $binary === '') {
|
||
throw new RuntimeException('WebP 编码失败。');
|
||
}
|
||
|
||
return $binary;
|
||
}
|
||
}
|