Kaynağa Gözat

优化ffmpeg和gd库超分图片功能

lh 1 ay önce
ebeveyn
işleme
e2178d1957

+ 11 - 11
app/Http/Controllers/AIGeneration/ImageGenerationController.php

@@ -191,9 +191,9 @@ class ImageGenerationController extends BaseController
         $rules = [
             'image_url'      => 'nullable|string',
             'image_file'     => 'nullable|image|mimes:jpeg,png,webp|max:13312',
-            'target'         => 'nullable|string|in:1080p,2k,4k',
-            'width'          => 'nullable|numeric|between:1,8192',
-            'height'         => 'nullable|numeric|between:1,8192',
+            'target_size'    => 'nullable|string|in:1080p,2k,4k',
+            'target_width'   => 'nullable|numeric|between:1,8192',
+            'target_height'  => 'nullable|numeric|between:1,8192',
             'format'         => 'nullable|string|in:jpg,jpeg,png,webp',
             'quality'        => 'nullable|numeric|between:1,100',
             'sharpen'        => 'nullable|boolean',
@@ -206,11 +206,11 @@ class ImageGenerationController extends BaseController
             'image_file.image'         => '上传文件必须是图片',
             'image_file.mimes'         => '图片格式必须是jpeg、png或webp',
             'image_file.max'           => '图片大小不能超过13MB',
-            'target.in'                => '目标档位必须是1080p、2k或4k',
-            'width.numeric'            => '宽度必须是数字',
-            'width.between'            => '宽度必须在1到8192之间',
-            'height.numeric'           => '高度必须是数字',
-            'height.between'           => '高度必须在1到8192之间',
+            'target_size.in'           => '目标档位必须是1080p、2k或4k',
+            'target_width.numeric'     => '宽度必须是数字',
+            'target_width.between'     => '宽度必须在1到8192之间',
+            'target_height.numeric'    => '高度必须是数字',
+            'target_height.between'    => '高度必须在1到8192之间',
             'format.in'                => '输出格式必须是jpg、png或webp',
             'quality.numeric'          => '输出质量必须是数字',
             'quality.between'          => '输出质量必须在1到100之间',
@@ -263,9 +263,9 @@ class ImageGenerationController extends BaseController
         // 组装超分参数(null 自动走默认值)
         $upscaleParams = [
             'image_url'      => $imageUrl,
-            'target'         => $data['target'] ?? null,
-            'width'          => isset($data['width']) ? (int)$data['width'] : null,
-            'height'         => isset($data['height']) ? (int)$data['height'] : null,
+            'target'         => $data['target_size'] ?? null,
+            'width'          => isset($data['target_width']) ? (int)$data['target_width'] : null,
+            'height'         => isset($data['target_height']) ? (int)$data['target_height'] : null,
             'format'         => $data['format'] ?? null,
             'quality'        => isset($data['quality']) ? (int)$data['quality'] : null,
             'sharpen'        => isset($data['sharpen']) ? filter_var($data['sharpen'], FILTER_VALIDATE_BOOLEAN) : null,

+ 115 - 7
app/Libs/Helpers.php

@@ -4129,7 +4129,8 @@ function safeDestroyImage(&$img)
  *  - target         string  可选 目标档位:1080p / 2k / 4k,默认 4k(按长边放大)
  *  - width          int     可选 指定目标宽度(与 height 同时传时优先于 target)
  *  - height         int     可选 指定目标高度
- *  - format         string  可选 输出格式:jpg / png / webp,默认保持原图格式(gif 转 png)
+ *  - format         string  可选 输出格式:jpg / png / webp,默认保持原图格式
+ *                            (gif 转 png;webp 带透明转 png、无透明转 jpg)
  *  - quality        int     可选 输出质量 1-100,默认 95(jpg/webp 生效)
  *  - sharpen        bool    可选 是否锐化,默认 true
  *  - sharpen_amount float   可选 锐化强度 0-2,默认 0.35
@@ -4161,6 +4162,8 @@ function upscaleImage(array $params): array
     $srcWidth = max(1, (int)$info[0]);
     $srcHeight = max(1, (int)$info[1]);
     $srcType = (int)($info[2] ?? IMAGETYPE_PNG);
+    // WebP 默认输出格式取决于是否带透明通道(有透明转PNG保留透明,无透明转JPG控制体积)
+    $srcHasAlpha = $srcType === IMAGETYPE_WEBP && upscaleImageHasAlpha($binary);
 
     // 计算目标尺寸
     [$dstWidth, $dstHeight] = upscaleImageResolveTargetDimensions($srcWidth, $srcHeight, $params);
@@ -4214,6 +4217,7 @@ function upscaleImage(array $params): array
             $params,
             $binary,
             $srcType,
+            $srcHasAlpha,
             $srcWidth,
             $srcHeight,
             $dstWidth,
@@ -4225,6 +4229,7 @@ function upscaleImage(array $params): array
         $params,
         $binary,
         $srcType,
+        $srcHasAlpha,
         $srcWidth,
         $srcHeight,
         $dstWidth,
@@ -4238,6 +4243,7 @@ function upscaleImage(array $params): array
  * @param array $params
  * @param string $binary 图片二进制内容
  * @param int $srcType 原图类型(IMAGETYPE_*)
+ * @param bool $srcHasAlpha 原图是否带透明通道(WebP 默认格式判断用)
  * @param int $srcWidth
  * @param int $srcHeight
  * @param int $dstWidth
@@ -4249,11 +4255,18 @@ function upscaleImageWithGd(
     array $params,
     string $binary,
     int $srcType,
+    bool $srcHasAlpha,
     int $srcWidth,
     int $srcHeight,
     int $dstWidth,
     int $dstHeight
 ): array {
+    // 4K 放大峰值内存较高:目标图(4B/像素) + 锐化卷积临时缓冲(4B/像素) + 源图 + 编码缓冲,
+    // 按需临时提升内存限制,避免超过默认 128M 报 FatalError
+    upscaleImageEnsureMemory(
+        $dstWidth * $dstHeight * 8 + $srcWidth * $srcHeight * 4 + 96 * 1024 * 1024
+    );
+
     $srcImg = @imagecreatefromstring($binary);
     if (!$srcImg) {
         throw new \RuntimeException('图片解码失败');
@@ -4268,6 +4281,9 @@ function upscaleImageWithGd(
             throw new \RuntimeException('图片放大失败');
         }
 
+        // 缩放完成后原图不再需要,提前释放以降低内存峰值
+        safeDestroyImage($srcImg);
+
         // 可选锐化,弥补插值放大后的发虚感
         $sharpen = (bool)($params['sharpen'] ?? true);
         $sharpenAmount = (float)($params['sharpen_amount'] ?? 0.35);
@@ -4276,7 +4292,7 @@ function upscaleImageWithGd(
         }
 
         // 确定输出格式并编码
-        $format = upscaleImageResolveOutputFormat($params, $srcType);
+        $format = upscaleImageResolveOutputFormat($params, $srcType, $srcHasAlpha);
         $quality = max(1, min(100, (int)($params['quality'] ?? 95)));
 
         $encoded = upscaleImageEncode($dstImg, $format, $quality);
@@ -4308,6 +4324,7 @@ function upscaleImageWithGd(
  * @param array $params
  * @param string $binary 图片二进制内容
  * @param int $srcType 原图类型(IMAGETYPE_*)
+ * @param bool $srcHasAlpha 原图是否带透明通道(WebP 默认格式判断用)
  * @param int $srcWidth
  * @param int $srcHeight
  * @param int $dstWidth
@@ -4319,6 +4336,7 @@ function upscaleImageWithFfmpeg(
     array $params,
     string $binary,
     int $srcType,
+    bool $srcHasAlpha,
     int $srcWidth,
     int $srcHeight,
     int $dstWidth,
@@ -4332,7 +4350,7 @@ function upscaleImageWithFfmpeg(
         @mkdir($tempDir, 0775, true);
     }
 
-    $format = upscaleImageResolveOutputFormat($params, $srcType);
+    $format = upscaleImageResolveOutputFormat($params, $srcType, $srcHasAlpha);
     $ext = $format === 'jpeg' ? 'jpg' : $format;
     $quality = max(1, min(100, (int)($params['quality'] ?? 95)));
 
@@ -4404,6 +4422,48 @@ function upscaleImageGdAvailable(): bool
 }
 
 /**
+ * 获取当前 PHP 内存限制(字节),-1 表示无限制
+ *
+ * @return int
+ */
+function upscaleImageMemoryLimitBytes(): int
+{
+    $val = trim((string)ini_get('memory_limit'));
+    if ($val === '' || $val === '-1') {
+        return -1;
+    }
+    $unit = strtoupper(substr($val, -1));
+    $num = (float)$val;
+    switch ($unit) {
+        case 'G':
+            return (int)($num * 1024 * 1024 * 1024);
+        case 'M':
+            return (int)($num * 1024 * 1024);
+        case 'K':
+            return (int)($num * 1024);
+        default:
+            return (int)$num;
+    }
+}
+
+/**
+ * 确保 PHP 内存限制满足图片超分需要(只升不降;已是无限制时不做处理)
+ *
+ * 4K 放大时源图解码、目标图、锐化卷积缓冲同时存在,峰值内存较大,
+ * 按目标尺寸估算并临时调高 memory_limit,避免 FatalError OOM。
+ *
+ * @param int $neededBytes 预估需要的内存字节数
+ * @return void
+ */
+function upscaleImageEnsureMemory(int $neededBytes): void
+{
+    $current = upscaleImageMemoryLimitBytes();
+    if ($current > 0 && $neededBytes > $current) {
+        @ini_set('memory_limit', (string)(int)ceil($neededBytes / (1024 * 1024)) . 'M');
+    }
+}
+
+/**
  * 检查 ffmpeg 是否可用
  *
  * @param string $ffmpegPath
@@ -4450,30 +4510,33 @@ function upscaleImageBuildFfmpegEncodeArgs(string $format, int $quality): string
  *
  * @param array $params
  * @param int $srcType
+ * @param bool $srcHasAlpha 原图是否带透明通道
  * @return string
  */
-function upscaleImageResolveOutputFormat(array $params, int $srcType): string
+function upscaleImageResolveOutputFormat(array $params, int $srcType, bool $srcHasAlpha = false): string
 {
     $format = strtolower((string)($params['format'] ?? ''));
     if (in_array($format, ['jpg', 'jpeg', 'png', 'webp'], true)) {
         return $format;
     }
-    return upscaleImageGuessFormatByType($srcType);
+    return upscaleImageGuessFormatByType($srcType, $srcHasAlpha);
 }
 
 /**
  * 根据图片类型推断默认输出格式
  *
  * @param int $srcType
+ * @param bool $srcHasAlpha 原图是否带透明通道
  * @return string
  */
-function upscaleImageGuessFormatByType(int $srcType): string
+function upscaleImageGuessFormatByType(int $srcType, bool $srcHasAlpha = false): string
 {
     switch ($srcType) {
         case IMAGETYPE_PNG:
             return 'png';
         case IMAGETYPE_WEBP:
-            return function_exists('imagewebp') ? 'webp' : 'jpg';
+            // 带透明通道的 WebP 转 PNG 保留透明,否则转 JPG 控制体积
+            return $srcHasAlpha ? 'png' : 'jpg';
         case IMAGETYPE_GIF:
             return 'png'; // GIF 转 PNG,保留透明通道
         case IMAGETYPE_JPEG:
@@ -4483,6 +4546,48 @@ function upscaleImageGuessFormatByType(int $srcType): string
 }
 
 /**
+ * 检测图片二进制是否带透明通道(目前用于 WebP 默认输出格式判断)
+ *
+ * 通过解析容器头判断,不整图解码,速度快:
+ *  - WebP: ALPH 块存在、VP8X 扩展头的 ALPHA 标志位、VP8L 无损格式的 alpha 标志
+ *
+ * @param string $binary
+ * @return bool
+ */
+function upscaleImageHasAlpha(string $binary): bool
+{
+    if (strlen($binary) < 16 || substr($binary, 0, 4) !== 'RIFF' || substr($binary, 8, 4) !== 'WEBP') {
+        return false;
+    }
+
+    $offset = 12;
+    $len = strlen($binary);
+    while ($offset + 8 <= $len) {
+        $fourcc = substr($binary, $offset, 4);
+        $chunkSize = (int)unpack('V', substr($binary, $offset + 4, 4))[1];
+
+        if ($fourcc === 'ALPH') {
+            return true;
+        }
+        if ($fourcc === 'VP8X' && $chunkSize >= 1) {
+            // VP8X 扩展头标志字节:bit4(0x10)=ALPHA
+            return (ord($binary[$offset + 8]) & 0x10) !== 0;
+        }
+        if ($fourcc === 'VP8L' && $chunkSize >= 1) {
+            // VP8L 无损格式:首字节 bit2=alpha_is_used
+            return ((ord($binary[$offset + 8]) >> 2) & 1) !== 0;
+        }
+        if ($chunkSize <= 0) {
+            break;
+        }
+        // RIFF 块数据为奇数长度时补 1 字节对齐
+        $offset += 8 + $chunkSize + ($chunkSize % 2);
+    }
+
+    return false;
+}
+
+/**
  * 使用卷积矩阵对图片进行锐化(C级实现,速度快)
  *
  * @param resource|\GdImage $img
@@ -4519,6 +4624,8 @@ function upscaleImageEncode($img, string $format, int $quality): ?string
                 imagejpeg($img, null, $quality);
                 break;
             case 'png':
+                // 显式开启 alpha 通道保存,否则透明区域会丢失
+                imagesavealpha($img, true);
                 // quality 100-1 映射到 PNG 压缩级别 0-9
                 imagepng($img, null, (int)round((100 - $quality) / 10));
                 break;
@@ -4526,6 +4633,7 @@ function upscaleImageEncode($img, string $format, int $quality): ?string
                 if (!function_exists('imagewebp')) {
                     throw new \RuntimeException('GD扩展不支持WebP输出');
                 }
+                imagesavealpha($img, true);
                 imagewebp($img, null, $quality);
                 break;
             default: