正在加载...

论Typecho上传图片并压缩为Webp加水印

建站那些事  ·  2025-08-26

预计阅读时间: 142.3 分钟
35335 字2 图250 字/分
⚠️
本文有一定时效性·1个月前更新
最后更新: 2025年08月26日

这段时间网站里的文章逐渐多起来了,但是我注意到加载多图文章的时候速度十分感人

在前端看了一下,发现Typecho默认的上传图片居然是PNG,一点没有压缩过的原图

这样本就让我20Mbps的服务器更加雪上加霜了

于是痛定思痛,决定把图片全部换成webp并且加上水印防止偷图,所以我在Github上找到了一个压缩加水印插件

xxhzm/Watermark: 一个功能强大的Typecho图片处理插件,支持自动添加文字水印、WebP格式转换等功能

image.png

我安装上以后发现完全没用,于是又找了一大堆的其他插件,依然没用

后来反应过来,我用的是富文本编辑器UEditor,它的上传接口不是Typecho的默认接口

于是乎,我翻了翻插件的配置文件,注意到了

        // 服务器统一请求接口路径
        //serverUrl: "/ueditor-plus/_demo_server/handle.php",
        serverUrl: URL + "php/controller.php",

这个玩意

这就说明,UEditor Plus For Typecho这个插件的上传接口在这里

打开后终于发现了图片处理类的相关代码,在action_upload.php里

image.png

我在原来的action_upload.php的末尾添加了一些额外的处理

// 如果上传成功且是图片,则进行图片处理
if ($fileInfo['state'] === 'SUCCESS' && in_array($_GET['action'], ['uploadimage', 'uploadscrawl'])) {
    // 获取插件配置
    $pluginConfig = \Typecho\Widget::widget('Widget_Options')->plugin('UEditorPlus');
    
    // 获取文件的完整路径
    $filePath = $_SERVER['DOCUMENT_ROOT'] . $fileInfo['url'];
    
    // 检查文件是否存在
    if (file_exists($filePath)) {
        $imageProcessor = new ImageProcessor();
        
        // 处理图片(水印 + WebP转换)
        $result = $imageProcessor->processImage($filePath, $pluginConfig, $fileInfo);
        
        // 如果处理成功,更新文件信息
        if ($result && isset($result['url'])) {
            $fileInfo['url'] = $result['url'];
            $fileInfo['title'] = $result['title'];
            $fileInfo['type'] = $result['type'];
            $fileInfo['size'] = $result['size'];
        }
    }
}

/* 返回数据 */
return json_encode($fileInfo);

再在相同位置创建一个imageprocessor.php文件用于处理水印和压缩相关的操作,大体思路是,先检测图片的大小有没有到阈值,如果到了,就将其压缩为webp格式并添加上水印,然后给这些字添加个黑色边框,防止在纯色背景下看不到

但注意,转换为Webp格式需要PHP有GD拓展,不过宝塔默认的PHP已经把这个拓展安装好了,就不需要额外操作了

于是叫AI猛地一通操作,就把imageprocessor.php给造出来了,最后回到这个插件本身的Plugin.php里,添加一些必要的配置项

<?php
/**
 * 图片处理类
 * 功能:WebP转换、添加水印
 * 注意:只是演示代码,无法直接copy过去用喔
 */
class ImageProcessor 
{
    /**
     * 处理上传的图片
     * @param string $filePath 图片文件路径
     * @param object $config 插件配置
     * @param array $fileInfo 文件信息
     * @return array|false 处理结果
     */
    public function processImage($filePath, $config, $fileInfo) 
    {
        // 检查是否为图片文件
        if (!$this->isImage($filePath)) {
            return false;
        }
        
        // 获取图片信息
        $imageInfo = getimagesize($filePath);
        if (!$imageInfo) {
            return false;
        }
        
        $width = $imageInfo[0];
        $height = $imageInfo[1];
        $type = $imageInfo[2];
        
        // 检查图片尺寸是否达到处理阈值
        $minWidth = isset($config->minWidth) ? intval($config->minWidth) : 200;
        $minHeight = isset($config->minHeight) ? intval($config->minHeight) : 200;
        
        if ($width < $minWidth || $height < $minHeight) {
            return false;
        }
        
        // 创建图片资源
        $image = $this->createImageResource($filePath, $type);
        if (!$image) {
            return false;
        }
        
        // 添加水印
        if (isset($config->watermarkEnable) && $config->watermarkEnable == '1') {
            $image = $this->addWatermark($image, $width, $height, $config);
        }
        
        $result = $fileInfo;
        
        // WebP转换
        if (isset($config->webpEnable) && $config->webpEnable == '1' && function_exists('imagewebp')) {
            $webpPath = $this->convertToWebP($image, $filePath, $config);
            if ($webpPath) {
                // 安全修改:移除直接使用服务器文档根目录的代码
                $result['url'] = $this->getRelativePath($webpPath); // 使用相对路径
                $result['title'] = basename($webpPath);
                $result['type'] = '.webp';
                $result['size'] = filesize($webpPath);
                
                // 是否删除原图
                if (!isset($config->webpKeepOriginal) || $config->webpKeepOriginal != '1') {
                    unlink($filePath);
                }
            }
        } else {
            // 不转WebP,但需要保存添加水印后的图片
            if (isset($config->watermarkEnable) && $config->watermarkEnable == '1') {
                $this->saveImage($image, $filePath, $type);
                $result['size'] = filesize($filePath);
            }
        }
        
        // 清理内存
        imagedestroy($image);
        
        return $result;
    }
    
    /**
     * 检查是否为图片文件
     */
    private function isImage($filePath) 
    {
        $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
        return in_array($ext, ['jpg', 'jpeg', 'png', 'gif']);
    }
    
    /**
     * 创建图片资源
     */
    private function createImageResource($filePath, $type) 
    {
        switch ($type) {
            case IMAGETYPE_JPEG:
                return imagecreatefromjpeg($filePath);
            case IMAGETYPE_PNG:
                return imagecreatefrompng($filePath);
            case IMAGETYPE_GIF:
                return imagecreatefromgif($filePath);
            default:
                return false;
        }
    }
    
    /**
     * 添加水印
     */
    private function addWatermark($image, $width, $height, $config) 
    {
        // 检查是否有水印图片
        if (!empty($config->watermarkImage) && file_exists($config->watermarkImage)) {
            return $this->addImageWatermark($image, $width, $height, $config);
        } 
        // 使用文字水印
        elseif (!empty($config->watermarkText)) {
            return $this->addTextWatermark($image, $width, $height, $config);
        }
        
        return $image;
    }
    
    /**
     * 添加图片水印
     */
    private function addImageWatermark($image, $width, $height, $config) 
    {
        $watermarkPath = $config->watermarkImage;
        $watermarkInfo = getimagesize($watermarkPath);
        
        if (!$watermarkInfo) {
            return $image;
        }
        
        // 创建水印图片资源
        $watermark = $this->createImageResource($watermarkPath, $watermarkInfo[2]);
        if (!$watermark) {
            return $image;
        }
        
        $wmWidth = $watermarkInfo[0];
        $wmHeight = $watermarkInfo[1];
        
        // 计算水印位置
        $position = $this->calculatePosition($width, $height, $wmWidth, $wmHeight, $config);
        
        // 获取透明度
        $opacity = isset($config->watermarkOpacity) ? intval($config->watermarkOpacity) : 50;
        
        // 添加水印
        $this->imagecopymerge_alpha($image, $watermark, $position['x'], $position['y'], 0, 0, $wmWidth, $wmHeight, $opacity);
        
        imagedestroy($watermark);
        return $image;
    }
    
    /**
     * 添加文字水印(带黑边效果,不加粗)
     */
    private function addTextWatermark($image, $width, $height, $config) 
    {
        $text = $config->watermarkText;
        $fontSize = isset($config->watermarkFontSize) ? intval($config->watermarkFontSize) : 24;
        
        // 获取不透明度设置
        $opacity = isset($config->watermarkOpacity) ? intval($config->watermarkOpacity) : 100;
        $alpha = 127 - intval($opacity * 127 / 100);
        
        // 定义颜色:白色文字,黑色边框
        $textColor = imagecolorallocatealpha($image, 255, 255, 255, $alpha);
        $borderColor = imagecolorallocatealpha($image, 0, 0, 0, $alpha);
        
        // 安全修改:字体文件路径应从配置获取,而非硬编码
        $fontPath = isset($config->fontPath) ? $config->fontPath : null;
        $useTTF = !empty($fontPath) && file_exists($fontPath);
        
        if ($useTTF) {
            // 使用TTF字体计算文字尺寸
            $textBox = imagettfbbox($fontSize, 0, $fontPath, $text);
            $textWidth = abs($textBox[4] - $textBox[0]);
            $textHeight = abs($textBox[5] - $textBox[1]);
        } else {
            // 使用内置字体
            $fontSize = 5;
            $textWidth = strlen($text) * 10;
            $textHeight = 15;
        }
        
        // 计算文字位置
        $position = $this->calculatePosition($width, $height, $textWidth, $textHeight, $config);
        $x = $position['x'];
        $y = $position['y'];
        
        // 绘制文字水印
        if ($useTTF) {
            $baseY = $y + $textHeight;
            
            // 1. 绘制黑边(8方向描边)
            $borderOffsets = [
                [-1, -1], [0, -1], [1, -1],
                [-1,  0],           [1,  0],
                [-1,  1], [0,  1], [1,  1]
            ];
            
            foreach ($borderOffsets as $offset) {
                imagettftext($image, $fontSize, 0, $x + $offset[0], $baseY + $offset[1], $borderColor, $fontPath, $text);
            }
            
            // 2. 绘制主文字
            imagettftext($image, $fontSize, 0, $x, $baseY, $textColor, $fontPath, $text);
            
        } else {
            // 使用内置字体
            
            // 1. 绘制黑边
            $borderOffsets = [
                [-1, -1], [0, -1], [1, -1],
                [-1,  0],           [1,  0],
                [-1,  1], [0,  1], [1,  1]
            ];
            
            foreach ($borderOffsets as $offset) {
                imagestring($image, $fontSize, $x + $offset[0], $y + $offset[1], $text, $borderColor);
            }
            
            // 2. 绘制主文字
            imagestring($image, $fontSize, $x, $y, $text, $textColor);
        }
        
        return $image;
    }
    
    /**
     * 计算水印位置
     */
    private function calculatePosition($imgWidth, $imgHeight, $wmWidth, $wmHeight, $config) 
    {
        $margin = isset($config->watermarkMargin) ? intval($config->watermarkMargin) : 10;
        $position = isset($config->watermarkPosition) ? $config->watermarkPosition : 'bottom-right';
        
        switch ($position) {
            case 'top-left':
                return ['x' => $margin, 'y' => $margin];
            case 'top-right':
                return ['x' => $imgWidth - $wmWidth - $margin, 'y' => $margin];
            case 'bottom-left':
                return ['x' => $margin, 'y' => $imgHeight - $wmHeight - $margin];
            case 'bottom-right':
                return ['x' => $imgWidth - $wmWidth - $margin, 'y' => $imgHeight - $wmHeight - $margin];
            case 'center':
                return ['x' => ($imgWidth - $wmWidth) / 2, 'y' => ($imgHeight - $wmHeight) / 2];
            default:
                return ['x' => $imgWidth - $wmWidth - $margin, 'y' => $imgHeight - $wmHeight - $margin];
        }
    }
    
    /**
     * 支持透明度的图片合并函数
     */
    private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) 
    {
        if (!isset($pct)) {
            return false;
        }
        
        $pct /= 100;
        
        // 获取图片的宽度和高度
        $w = imagesx($src_im);
        $h = imagesy($src_im);
        
        // 创建一个临时图片
        $cut = imagecreatetruecolor($src_w, $src_h);
        
        // 拷贝源图片的相应部分到临时图片
        imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h);
        
        // 拷贝要合并的图片到临时图片
        imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h);
        
        // 设置临时图片为混合模式
        imagealphablending($dst_im, true);
        
        // 将临时图片合并到目标图片
        imagecopymerge($dst_im, $cut, $dst_x, $dst_y, 0, 0, $src_w, $src_h, $pct * 100);
        
        imagedestroy($cut);
    }
    
    /**
     * 转换为WebP格式
     */
    private function convertToWebP($image, $originalPath, $config) 
    {
        $quality = isset($config->webpQuality) ? intval($config->webpQuality) : 80;
        $webpPath = preg_replace('/\.(jpg|jpeg|png|gif)$/i', '.webp', $originalPath);
        
        // 设置WebP支持透明度
        imagesavealpha($image, true);
        
        if (imagewebp($image, $webpPath, $quality)) {
            return $webpPath;
        }
        
        return false;
    }
    
    /**
     * 保存图片
     */
    private function saveImage($image, $filePath, $type) 
    {
        switch ($type) {
            case IMAGETYPE_JPEG:
                return imagejpeg($image, $filePath, 90);
            case IMAGETYPE_PNG:
                imagesavealpha($image, true);
                return imagepng($image, $filePath);
            case IMAGETYPE_GIF:
                return imagegif($image, $filePath);
            default:
                return false;
        }
    }
    
    /**
     * 安全修改:获取相对路径的方法
     * 替代直接使用 $_SERVER['DOCUMENT_ROOT']
     */
    private function getRelativePath($fullPath) 
    {
        // 这里应该根据实际情况实现路径转换逻辑
        // 示例实现 - 实际使用时需要根据项目结构调整
        $basePath = defined('BASE_UPLOAD_PATH') ? BASE_UPLOAD_PATH : '/uploads/';
        return $basePath . basename($fullPath);
    }
}
    public static function config(Form $form) {
        // WebP转换设置
        $webpEnable = new Radio('webpEnable', array('1' => '启用', '0' => '禁用'), '0', 'WebP转换', '启用后会自动将上传的PNG、JPG图片转换为WebP格式');
        $form->addInput($webpEnable);
        
        $webpQuality = new Text('webpQuality', null, '80', 'WebP质量', '设置WebP图片质量,范围1-100,数值越高质量越好但文件越大');
        $form->addInput($webpQuality);
        
        $webpKeepOriginal = new Radio('webpKeepOriginal', array('1' => '保留', '0' => '删除'), '0', '保留原图', '转换为WebP后是否保留原始图片文件');
        $form->addInput($webpKeepOriginal);
        
        // 水印设置
        $watermarkEnable = new Radio('watermarkEnable', array('1' => '启用', '0' => '禁用'), '0', '图片水印', '启用后会为上传的图片添加水印');
        $form->addInput($watermarkEnable);
        
        $watermarkImage = new Text('watermarkImage', null, '', '水印图片路径', '水印图片的完整路径,支持PNG格式(推荐使用透明背景)');
        $form->addInput($watermarkImage);
        
        $watermarkText = new Text('watermarkText', null, '', '文字水印', '如果不设置水印图片,可以使用文字水印');
        $form->addInput($watermarkText);
        
        $watermarkFontSize = new Text('watermarkFontSize', null, '24', '水印字体大小', '设置文字水印的字体大小(像素),默认24');
        $form->addInput($watermarkFontSize);
        
        $watermarkPosition = new Select('watermarkPosition', array(
            'top-left' => '左上角',
            'top-right' => '右上角', 
            'bottom-left' => '左下角',
            'bottom-right' => '右下角',
            'center' => '居中'
        ), 'bottom-right', '水印位置', '选择水印在图片中的位置');
        $form->addInput($watermarkPosition);
        
        $watermarkOpacity = new Text('watermarkOpacity', null, '100', '水印不透明度', '设置水印不透明度,范围0-100,0为完全透明,100为完全不透明');
        $form->addInput($watermarkOpacity);
        
        $watermarkMargin = new Text('watermarkMargin', null, '10', '水印边距', '水印距离图片边缘的距离(像素)');
        $form->addInput($watermarkMargin);
        
        // 图片处理阈值设置
        $minWidth = new Text('minWidth', null, '200', '最小处理宽度', '只有宽度大于此值的图片才会被处理(像素)');
        $form->addInput($minWidth);
        
        $minHeight = new Text('minHeight', null, '200', '最小处理高度', '只有高度大于此值的图片才会被处理(像素)');
        $form->addInput($minHeight);
    }

就大功告成了,这下不用担心水管被图片挤爆了

评论
正在加载验证组件