2026-02-11 17:28:36 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
2026-02-12 17:10:36 +08:00
|
|
|
|
use App\Support\MarkdownRenderer;
|
2026-02-11 17:28:36 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-02-12 17:10:36 +08:00
|
|
|
|
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'] ?? '')),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 17:28:36 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-02-12 13:06:12 +08:00
|
|
|
|
$mainUrl = '/storage/'.ltrim($mainPath, '/');
|
|
|
|
|
|
$thumbUrl = '/storage/'.ltrim($thumbPath, '/');
|
2026-02-11 17:28:36 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|