DedeCMS 缩略图不变形裁剪算法详解:image.helper.php 重写、imagecopyresampled 与 WebP 双格式输出

DedeCMS 默认 ImageResize 等比缩放至矩形内会让宽高比不同的图被强行拉伸变形。本文从算法层剖析三种裁剪策略(等比/左上角/中心智能裁剪)的数学逻辑,给出含 WebP 支持、PNG 透明保留、人像上 1/3 智能偏移的 image.helper.php 完整重写,并扩展到 Imagick 替代、CDN 图像处理服务对比。

张文保 更新 29 分钟阅读 1,075 阅读

DedeCMS 默认的缩略图生成函数 ImageResize 用的是「等比例缩放到目标矩形内」算法,结果就是宽幅照片缩成 200x200 时被强行压扁,人脸变胖、商品变形。本文聚焦于 include/helpers/image.helper.php 这个文件里 ImageResize 函数的算法层细节:三种主流裁剪方案的数学逻辑(等比缩放 vs 左上角裁剪 vs 中心裁剪)、GD 函数 imagecopyresampled 的工作原理、Lanczos 与 Bicubic 插值的选择、现代化替代方案 Imagick 与 WebP 输出。

缩略图变形的根本原因

原版 ImageResize 函数的算法

打开 include/helpers/image.helper.php,定位 ImageResize 函数体内的关键算法段:

$srcW = ImageSX($im);
$srcH = ImageSY($im);
if ($srcW <= $toW && $srcH <= $toH) return TRUE;

$toWH = $toW / $toH;
$srcWH = $srcW / $srcH;
if ($toWH <= $srcWH) {
    $ftoW = $toW;
    $ftoH = $ftoW * ($srcH / $srcW);
} else {
    $ftoH = $toH;
    $ftoW = $ftoH * ($srcW / $srcH);
}

问题剖析

这段代码是「等比缩放至目标矩形内接」算法。原图宽高比 srcWH 与目标宽高比 toWH 比较:

  • 原图比目标更宽(srcWH > toWH):以目标高度 toH 为基准等比缩放,结果宽度 ftoW 可能小于 toW。
  • 原图比目标更高(srcWH < toWH):以目标宽度 toW 为基准等比缩放,结果高度 ftoH 可能小于 toH。

看似合理,但 imagecopyresampled 把缩放后的画布尺寸(ftoW × ftoH)直接拉伸到目标尺寸(toW × toH),这一步把原本的等比例破坏了。原图 1200×800(3:2)被强行拉到 200×200(1:1),横向被压缩 1.5 倍——人脸明显变胖。

问题的本质:算法只解决了「填进容器」但没决定「容器外多余部分怎么处理」(压扁还是裁剪)。要做不变形缩略图,必须明确选择「裁剪」策略。

方案一:左上角裁剪(最简单但不智能)

算法逻辑

$srcW = ImageSX($im);
$srcH = ImageSY($im);
if ($srcW <= $toW && $srcH <= $toH) return TRUE;

$toWH = $toW / $toH;
$srcWH = $srcW / $srcH;
$ftoH = $toH;
$ftoW = $toW;

if ($toWH <= $srcWH) {
    /* 原图更扁:水平方向裁剪 */
    $src_Y = 0;
    $src_X = ($srcW - $srcH * $toWH) / 2;
    $srcW = $srcH * $toWH;
} else {
    /* 原图更高:垂直方向裁剪 */
    $src_X = 0;
    $src_Y = ($srcH - $srcW / $toWH) / 2;
    $srcH = $srcW / $toWH;
}

关键变化

计算 src_X 与 src_Y 偏移量,让 imagecopyresampled 时只取原图的中央矩形区域而不是整张图。

以宽 1200 × 高 800 原图缩到 200×200 为例:

  • toWH = 200/200 = 1.0
  • srcWH = 1200/800 = 1.5
  • toWH ≤ srcWH 进入第一分支
  • src_X = (1200 - 800 × 1.0) / 2 = 200,从原图 X=200 处开始截取
  • 截取的宽度 = 800 × 1.0 = 800(与高度相等,为 1:1 方形)
  • 结果:从原图中央截取 800×800 的方形,再缩到 200×200

这个版本能保持比例,但裁剪基准是「中心」(src_X 取了 (srcW - cropW)/2 即水平居中),而不是原文宣称的「左上角」。原文文字描述与代码逻辑不一致。真正的左上角裁剪是 src_X = 0、src_Y = 0,本文这版实际已经是中心裁剪。

方案二:智能中心裁剪(推荐)

完整函数重写

把整个 ImageResize 函数体替换为更完善的版本:

function ImageResize($srcFile, $toW, $toH, $toFile = "") {
    global $cfg_photo_type;
    if ($toFile == "") $toFile = $srcFile;

    $srcInfo = GetImageSize($srcFile);
    if (empty($srcInfo)) return false;

    /* 按文件类型创建源图像资源 */
    switch ($srcInfo[2]) {
        case IMAGETYPE_GIF:
            if (!$cfg_photo_type['gif']) return false;
            $im = imagecreatefromgif($srcFile);
            break;
        case IMAGETYPE_JPEG:
            if (!$cfg_photo_type['jpeg']) return false;
            $im = imagecreatefromjpeg($srcFile);
            break;
        case IMAGETYPE_PNG:
            if (!$cfg_photo_type['png']) return false;
            $im = imagecreatefrompng($srcFile);
            break;
        case IMAGETYPE_WEBP:
            if (!function_exists('imagecreatefromwebp')) return false;
            $im = imagecreatefromwebp($srcFile);
            break;
        case IMAGETYPE_BMP:
            if (!$cfg_photo_type['bmp']) return false;
            $im = imagecreatefromwbmp($srcFile);
            break;
        default:
            return false;
    }
    if (!$im) return false;

    $srcW = imagesx($im);
    $srcH = imagesy($im);

    /* 原图小于目标尺寸:不放大,直接复制原文件 */
    if ($srcW <= $toW && $srcH <= $toH) {
        if ($srcFile != $toFile) {
            copy($srcFile, $toFile);
        }
        imagedestroy($im);
        return true;
    }

    $srcRatio = $srcW / $srcH;
    $dstRatio = $toW / $toH;

    /* 计算从原图裁出的矩形(中心对齐) */
    if ($srcRatio > $dstRatio) {
        /* 原图比目标更宽:左右裁掉 */
        $cropH = $srcH;
        $cropW = (int)($srcH * $dstRatio);
        $cropX = (int)(($srcW - $cropW) / 2);
        $cropY = 0;
    } else {
        /* 原图比目标更高:上下裁掉(多数选择保留上半部分) */
        $cropW = $srcW;
        $cropH = (int)($srcW / $dstRatio);
        $cropX = 0;
        /* 智能策略:人像照保留上 1/3 而不是中心 */
        $cropY = (int)(($srcH - $cropH) / 3);
    }

    /* 创建目标图像(带透明色支持) */
    $newImg = imagecreatetruecolor($toW, $toH);
    if ($srcInfo[2] == IMAGETYPE_PNG || $srcInfo[2] == IMAGETYPE_GIF) {
        imagealphablending($newImg, false);
        imagesavealpha($newImg, true);
        $transparent = imagecolorallocatealpha($newImg, 255, 255, 255, 127);
        imagefilledrectangle($newImg, 0, 0, $toW, $toH, $transparent);
    }

    /* 用 imagecopyresampled 做高质量缩放 */
    imagecopyresampled(
        $newImg, $im,
        0, 0,             /* 目标左上角 */
        $cropX, $cropY,   /* 源裁剪左上角 */
        $toW, $toH,       /* 目标宽高 */
        $cropW, $cropH    /* 源裁剪宽高 */
    );

    /* 输出 */
    switch ($srcInfo[2]) {
        case IMAGETYPE_GIF:
            imagegif($newImg, $toFile);
            break;
        case IMAGETYPE_JPEG:
            imagejpeg($newImg, $toFile, 88);
            break;
        case IMAGETYPE_PNG:
            imagepng($newImg, $toFile, 8);
            break;
        case IMAGETYPE_WEBP:
            imagewebp($newImg, $toFile, 85);
            break;
    }

    imagedestroy($newImg);
    imagedestroy($im);
    return true;
}

关键改进点

  1. 不放大原图:原图比目标小时直接 copy 文件,不做缩放(imagecopyresampled 放大会产生像素马赛克)。
  2. WebP 支持:增加 IMAGETYPE_WEBP 分支,让 PHP 7.4+ 能处理 WebP 输入与输出。
  3. 透明通道保留:PNG 与 GIF 透明色用 imagealphablending + imagesavealpha 保留,否则会变成黑底。
  4. 人像智能裁剪:垂直方向裁剪时取上 1/3 而不是中心,因为人像照人脸通常在画面上半部。这是简单的启发式规则,复杂场景需要人脸识别(见后文)。
  5. JPEG 质量 88:原版 90,改为 88 减小文件 25% 而画质损失肉眼难见(普遍认为 85-92 是甜蜜区)。
  6. PNG 压缩级别 8:默认 6,改为 8 进一步压缩 PNG 文件大小(最大 9,9 时压缩耗时翻倍)。

imagecopyresampled vs imagecopyresized

两个函数的差异

  • imagecopyresized:用「最近邻插值」(Nearest Neighbor),速度快,但缩放后图像有锯齿、马赛克感。1990 年代的 GD1 库默认算法。
  • imagecopyresampled:用「双线性插值」(Bilinear),算法上对周围 4 像素加权平均,缩略图平滑很多。GD2 起的默认算法。

所有现代代码都应该用 imagecopyresampled。原版 DedeCMS 的部分分支 fallback 到 imagecopyresized,那是 2008 年的代码兼容老服务器,现在都不需要。

Lanczos 与 Bicubic 插值(更高级)

imagecopyresampled 的 Bilinear 在大幅缩小(10× 以上)时仍有锐度损失。要更优画质用 Imagick 库:

function ImagickResize($srcFile, $toW, $toH, $toFile) {
    $img = new Imagick($srcFile);
    $img->setImageCompressionQuality(88);

    /* Lanczos 插值,高画质 */
    $img->setImageInterpolateMethod(Imagick::INTERPOLATE_BICUBIC);
    $img->cropThumbnailImage($toW, $toH);

    /* 锐化弥补缩放模糊 */
    $img->sharpenImage(0, 0.5);

    $img->writeImage($toFile);
    $img->clear();
    $img->destroy();
    return true;
}

cropThumbnailImage 一行命令就完成「按比例裁剪 + 缩放」。比手写 GD 代码简洁 10 倍,画质也更好。前提是服务器装了 ImageMagick + php-imagick 扩展。

WebP 输出:缩略图体积减半

WebP 与 JPEG 的对比

同样 200×200 的缩略图:

  • JPEG 88 质量:18 KB
  • WebP 85 质量:9 KB(小 50%)
  • WebP 70 质量:6 KB(视觉上接近 JPEG 88)

WebP 在所有现代浏览器都支持(Chrome 32+、Firefox 65+、Safari 14+),缩略图列表页用 WebP 能让总下载量减半。

双格式输出策略

为了兼容老浏览器,可以同时输出 JPEG 与 WebP,前端按浏览器支持选择:

function ImageResizeDualFormat($srcFile, $toW, $toH, $toBaseFile) {
    $im = imagecreatefromjpeg($srcFile); /* 简化,省略类型判断 */
    /* ... 缩放逻辑同前 ... */

    /* 输出 JPEG */
    imagejpeg($newImg, $toBaseFile . '.jpg', 88);
    /* 输出 WebP */
    if (function_exists('imagewebp')) {
        imagewebp($newImg, $toBaseFile . '.webp', 85);
    }

    imagedestroy($newImg);
}

HTML 里用 picture 标签让浏览器自动选最优格式:

<picture>
    <source srcset="thumb.webp" type="image/webp">
    <img src="thumb.jpg" alt="...">
</picture>

批量重新生成历史缩略图

改造代码后,已经存在的缩略图仍是变形版本。需要写脚本批量重新生成。

命令行脚本

<?php
/* regen_thumbs.php - 批量重新生成 DedeCMS 缩略图 */
require_once 'include/common.inc.php';
require_once 'include/helpers/image.helper.php';

$dsql->SetQuery("SELECT id, litpic FROM #@__archives WHERE litpic LIKE '%/litimg/%'");
$dsql->Execute();

$count = 0;
$thumbW = $cfg_ddimg_width ?: 240;
$thumbH = $cfg_ddimg_height ?: 180;

while ($row = $dsql->GetArray()) {
    $litpic = $cfg_basedir . $row['litpic'];
    $original = str_replace('/litimg/', '/allimg/', $litpic);

    if (file_exists($original)) {
        $result = ImageResize($original, $thumbW, $thumbH, $litpic);
        if ($result) {
            $count++;
            echo "Regenerated: {$row['litpic']}\n";
        }
    }
}

echo "Done. Total: $count\n";
?>

放到 dede 目录下用命令行执行:php regen_thumbs.php

性能注意

1 万张缩略图重新生成大约 30-50 分钟(取决于原图大小与 CPU)。建议低峰期跑,或者分批处理(每次 1000 张)。

智能内容感知裁剪

问题:人像照人脸被裁掉

用户上传的人像照片,主体(脸)可能在画面任意位置。简单的中心裁剪会把脸切掉。

方案:人脸检测

用 PHP 调用 OpenCV 或第三方人脸识别 API:

function smartCrop($srcFile, $toW, $toH) {
    /* 调用百度 AI 人脸识别 API(或者 AWS Rekognition、Azure Face) */
    $faceData = callBaiduFaceAPI($srcFile);
    if (!empty($faceData['face_list'])) {
        $face = $faceData['face_list'][0];
        $faceX = $face['location']['left'];
        $faceY = $face['location']['top'];
        $faceW = $face['location']['width'];
        $faceH = $face['location']['height'];

        /* 裁剪框中心对齐人脸中心 */
        $cropCenterX = $faceX + $faceW / 2;
        $cropCenterY = $faceY + $faceH / 2;

        /* 计算裁剪框 */
        $cropX = max(0, $cropCenterX - $toW / 2);
        $cropY = max(0, $cropCenterY - $toH / 2);

        /* 用 imagecopyresampled 裁剪 */
        // ...
    }
}

更轻的方案:smartcrop.js / smartcrop-php

开源的 smartcrop 库基于「视觉显著性」算法(不依赖 AI),能识别图像中的「显著区域」(人脸、文字、高对比度物体):

composer require codertaylor/smartcrop-php

use SmartCrop\SmartCrop;

$crop = new SmartCrop($srcFile);
$result = $crop->analyse();
$cropX = $result->getTopCrop()->x;
$cropY = $result->getTopCrop()->y;
$cropW = $result->getTopCrop()->width;
$cropH = $result->getTopCrop()->height;

不依赖外部 API,部署简单,画质比中心裁剪好。

CDN 与外部图像处理服务

七牛云、又拍云、阿里云 OSS 图像处理

这些 CDN 服务提供 URL 参数即可生成不同尺寸缩略图:

原图:https://cdn.example.com/photo.jpg
缩略图 200×200 中心裁剪:https://cdn.example.com/photo.jpg?imageView2/1/w/200/h/200

这种方案的优点:

  • 无限尺寸(按需生成),不需要预生成存大量文件。
  • 边缘 CDN 缓存,全球加速。
  • 带 WebP 自动格式协商(Accept 头含 image/webp 时返回 WebP)。
  • 智能裁剪、人脸识别、马赛克、滤镜等高级功能内置。

缺点:依赖第三方服务,国内 CDN 月费几百到几千元。但相比自建图像处理流水线,性价比高。

Cloudflare Images

Cloudflare Images 月费 5 美元起,全球 CDN 覆盖。适合海外站点。

Imgix、Cloudinary

专业图像处理服务。Imgix 月费 75 美元起,Cloudinary 有免费额度(25 credits/月)。功能比国内 CDN 强但价格高。

常见故障

故障 1:替换函数后所有图都变黑

多数是 PNG 透明色处理缺失。本文版本里有 imagealphablending + imagesavealpha 处理。如果你用的是简化版没加这两行,就会触发。

故障 2:JPEG 输出文件大但画质看起来还差

imagejpeg 的质量参数 0-100,不是越高越好。88-92 是甜蜜区。低于 80 会有可见 artifact,高于 95 文件膨胀但视觉看不出差别。

故障 3:缩略图正常但商品列表页加载慢

缩略图本身 10-20 KB 不大,但一页 50 张图的 PLP 总下载量 500 KB-1 MB。改善:用 picture + WebP 格式让总下载量降到 250-500 KB;首屏图加 fetchpriority=high;非首屏图懒加载(loading=lazy)。

故障 4:Imagick 报「PolicyError: not authorized for type PDF」

ImageMagick 的 policy.xml 默认禁用某些格式。修改 /etc/ImageMagick-6/policy.xml 把 right="none" 改成 right="read|write"。

故障 5:批量重生成脚本中途中断

大量 GD 调用占内存,PHP-CLI 默认 memory_limit 可能不够。php -d memory_limit=512M regen_thumbs.php 临时增大;或者每处理 100 张主动 gc_collect_cycles() 释放循环引用。

故障 6:WebP 输出在 IE 11 上显示不出

IE 11 不支持 WebP。用 picture 标签做格式 fallback,IE 用户看 JPEG 版本。或者直接放弃 IE 11(市占率 < 0.3%)。

故障 7:CDN 图像处理服务费用上涨

CDN 服务多按「处理次数 + 流量」计费。如果你的站点突然爆款,账单可能翻 10 倍。优化:在 PHP 层加一层缓存(处理过的 URL 写入 Redis 缓存 24 小时),同一缩略图不重复请求 CDN 处理 API。

常见问题解答

裁剪与等比缩放怎么选?

电商商品列表(PLP)需要统一尺寸视觉整齐 → 裁剪。文章列表的特色图主题各异 → 等比缩放(保留长方形或方形多种比例)。混合站点可以为不同模板用不同函数。

缩略图应该多大尺寸?

移动端列表卡片 200×200 px 足够(DPR 2 设备实际显示 100×100 物理像素);桌面端列表 240×180;详情页主图 800×600 或 1200×800。建议生成 3-4 种尺寸覆盖不同设备。

原图应该保留还是删除?

保留。未来可能调整缩略图尺寸或者支持新格式(AVIF)需要重新生成。生产环境用对象存储(OSS/S3)保留原图,CDN 缓存缩略图。

imagecreatefromjpeg 报内存不足?

原图分辨率太大(手机拍的 4000×3000 照片解码后占 48 MB 内存)。提高 PHP memory_limit 到 256M;或者用 Imagick 的流式处理(不全部读进内存)。

能否做带圆角或边框的缩略图?

能。在 imagecopyresampled 之后用 imageellipse + imagesetpixel 画圆角,或者准备一张带圆角的 PNG mask 与缩略图合成。Imagick 用 borderImage 直接加边框。

缩略图水印如何处理?

imagecopy 把水印 PNG 复制到缩略图右下角。注意水印尺寸要按缩略图尺寸缩放,否则在小缩略图上水印可能比图本身还大。

历史缩略图删除后能恢复吗?

原图还在就能重新生成。原图也丢失就只能从备份恢复。这就是为什么强调原图必须保留。

本方法适用 DedeBIZ 吗?

适用。DedeBIZ 沿用 DedeCMS 的 image.helper.php 文件路径与函数签名,本文方案直接可用。

切到 Imagick 后性能比 GD 高吗?

单张处理 GD 略快(GD 是 C 直接调用,Imagick 多一层 PHP 扩展层)。但 Imagick 多线程支持好,批量处理时性能反超 GD。同时 Imagick 内存管理更好,处理 4K 图不容易爆内存。

能否把缩略图迁移到云存储?

能。生成时直接调云存储 API 上传,URL 写入 dede_archives.litpic 字段。这样本地服务器只存代码,图片走云存储 + CDN 加速。规模大的站点强烈推荐这种架构。

分享到
标签
版权声明

本文标题:《DedeCMS 缩略图不变形裁剪算法详解:image.helper.php 重写、imagecopyresampled 与 WebP 双格式输出》

本文链接:https://zhangwenbao.com/deformable-clipping-method-for-dedecms-thumbnails.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交