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' => '![](' . $mainUrl . ')', ]); } 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; } }