SwitchImges 插件,调用PHP GD/imagemagick/ffmpeg库上传图片并转换成webp/avif,根据文件大小阀值来判断是否添加水印。
原作者应该是: 苏晓晴
我对插件进行了修改,主要是修改ffmpeg部分,以及配置页面增加水印选择和文件阀值大小设定,日志输出到插件目录内。不过有个问题,虽然上传的是webp格式,并且无论大于或小于预设的文件大小阀值,从日志看还是会转换格式,并且源格式显示的是PNG,不过也不影响,我就没继续问AI修改了。
- 配置页面增加水印选择、文件大小阀值预设(防止封面添加上水印)
- 修改ffmpeg转换,添加水印支持
- webp、avif格式不进行转换,根据文件大小阀值预设来判断是否添加水印
- 基于SwitchImges 插件 二次修改,保留原功能
- 支持将 JPG,PNG,GIF(仅限FFMPEG)格式转换到 WEBP,AVIF格式
- 图片质量选择1-100
- 图片最大宽度、最大高度设定(留空表示无限制)
- 支持原图片备份,在/usr/uoloads/年份/月份/back,图片增加_back名称
- 增加日志输出、在插件文件夹内
- 水印图片放到插件文件夹内即可,插件配置页面会列出可用水印。


将下方代码保存为 Plugin.php ,并放到插件目录内,文件夹名字为:Switchlmges
服务器记得安装 FFmpeg,否者无法使用。
<?php
/**
* 调用PHP GD/imagemagick/ffmpeg库<br>上传图片并转换成webp/avif,根据文件大小阀值来判断是否添加水印<br ><a href="https://www.op404.com/archives/155.html" target="_blank" style="background: #000;padding: 2px 4px;color: #ffeb00;font-size: 12px;" rel="noopener noreferrer">由OP404二次修改</a>
*
* @package SwitchImges
* @author 苏晓晴,OP404
* @version 1.2.1
* @link https://www.toubiec.cn/
* @link https://www.op404.com/
*/
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
class SwitchImges_Plugin implements Typecho_Plugin_Interface
{
/**
* 激活插件方法,如果激活失败,直接抛出异常
*
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function activate()
{
//ini_set('memory_limit', '256M');
Typecho_Plugin::factory('Widget_Upload')->uploadHandle = array('SwitchImges_Plugin', 'uploadHandle');
return _t('插件已启用!');
}
/**
* 禁用插件方法
*
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function deactivate(){}
/**
* 获取插件配置面板
*
* @access public
* @param Typecho_Widget_Helper_Form $form 配置面板
* @return void
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
?>
<style>
span.code {
margin: 0 8px;
display: inline-block;
font-weight: bold;
cursor: pointer;
transition: all .2s;
border: 1px solid transparent;
padding: 0 4px;
}
span.code.success {
color: #5cb85c;
border-color: #5cb85c;
}
</style>.
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.typecho-option.click-to-input span.code').forEach(span => {
span.addEventListener('click', function (e) {
let current = e.target;
let input = current.closest('.typecho-option').querySelector('input');
input.value = current.innerText;
current.classList.add('success');
setTimeout(() => current.classList.remove('success'), 1000);
})
})
})
</script>
<?php
// 图片质量设置
$quality = new Typecho_Widget_Helper_Form_Element_Radio(
'quality',
array('60' => _t('60'),'65'=>_t('65'),'70'=>_t('70'),'75'=>_t('75'),'80'=>_t('80'),'85'=>_t('85'),'90'=>_t('90'),'95'=>_t('95'),'100'=>_t('100')),
'80','图片质量','设置转换后的图片格式质量(0-100)最佳80');
$form->addInput($quality);
// 最大宽度设置
$maxWidth = new Typecho_Widget_Helper_Form_Element_Text('maxWidth', null, '', _t('最大宽度'), _t('设置上传图片的最大宽度,留空表示无限制'));
$form->addInput($maxWidth);
// 最大高度设置
$maxHeight = new Typecho_Widget_Helper_Form_Element_Text('maxHeight', null, '', _t('最大高度'), _t('设置上传图片的最大高度,留空表示无限制'));
$form->addInput($maxHeight);
// 压缩格式选择:Webp、avif 或 tiff
$compressext = new Typecho_Widget_Helper_Form_Element_Radio(
'compressext',
array('webp' => _t('Webp'), 'avif' => _t('Avif')),
'webp',
_t('压缩格式'),
_t('选择使用 Webp、Avif 格式。')
);
$form->addInput($compressext);
// 压缩方法选择:GD、ImageMagick、cwebp、ffmpeg
$compressionMethod = new Typecho_Widget_Helper_Form_Element_Radio(
'compressionMethod',
array('gd' => _t('GD'), 'imagemagick' => _t('ImageMagick'), 'ffmpeg' => _t('ffmpeg')),
'gd',
_t('压缩方法'),
_t('选择使用 GD(静态)、ImageMagick(静态)、ffmpeg(动态/静态) 工具来进行图像压缩。')
);
$form->addInput($compressionMethod);
// GIF 处理方式选择
$gifHandling = new Typecho_Widget_Helper_Form_Element_Radio(
'gifHandling',
array('static' => _t('静态处理'), 'animated' => _t('动态处理')),
'static',
_t('GIF 处理方式'),
_t('选择如何处理 GIF 文件,静态格式 适用于非动态 JPG|PNG|JPGE,动态格式 适用于包含动画的 GIF。')
);
$form->addInput($gifHandling);
// 备份源文件开关
$backupOriginal = new Typecho_Widget_Helper_Form_Element_Radio(
'backupOriginal',
array('enable' => _t('启用'), 'disable' => _t('禁用')),
'disable',
_t('备份源文件'),
_t('启用此选项将会在转换为你所选择的压缩格式时当前目录下备份名为_backup的原始图像文件。')
);
$form->addInput($backupOriginal);
//$vm_type = new Typecho_Widget_Helper_Form_Element_Checkbox('vm_type',
//array('pic' => '图片',
//'text' => '禁用'),
//array('pic'), '水印类型');
//$form->addInput($vm_type);
// 水印路径
$images_directory = __DIR__ . DIRECTORY_SEPARATOR;
$image_files = scandir($images_directory);
$image_list = [];
foreach ($image_files as $file) {
// 检查是否是文件而不是目录,并且是图片文件(可以根据实际情况添加更多的图片文件扩展名)
if (is_file($images_directory . $file) && preg_match('/\.(jpg|jpeg|png)$/i', $file)) {
$image_list[] = $file;
}
}
//$url = Typecho_Widget::widget('Widget_Options')->index . "Action.php?clearBackup";
$vm_pic = new Typecho_Widget_Helper_Form_Element_Text('vm_pic', NULL, 'op404.png',_t('水印图片'), count($image_list) > 0 ? _t("可用图片: %s点击即可复制到输入框,若不行请手动复制", "<span class='code'>" . implode('</span>,<span class="code">', $image_list) . "</span>") : _t("目录中没有图片文件,无法使用图片水印功能"));
$vm_pic->input->setAttribute('class', 'mini');
$vm_pic->setAttribute('class', 'typecho-option click-to-input');
$form->addInput($vm_pic);
// 缩略图/封面大小
$fmSize = new Typecho_Widget_Helper_Form_Element_Text('fmSize', null, '', _t('缩略图/封面大小(KB)'), _t('设置封面/缩略图大小,免于水印(如果为空默认10KB=10240字节,内部自动 * 1024)'));
$form->addInput($fmSize);
}
/**
* 个人用户的配置面板
*
* @access public
* @param Typecho_Widget_Helper_Form $form
* @return void
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form){}
/**
* 获取插件设置
*
* @access private
* @return array
*/
private static function getSettings()
{
$options = Typecho_Widget::widget('Widget_Options');
$maxWidth = $options->plugin('SwitchImges')->maxWidth;
$maxHeight = $options->plugin('SwitchImges')->maxHeight;
$fmSize = $options->plugin('SwitchImges')->fmSize;
return array(
'quality' => (int)$options->plugin('SwitchImges')->quality,
'maxWidth' => $maxWidth === '' ? null : (int)$maxWidth,
'maxHeight' => $maxHeight === '' ? null : (int)$maxHeight,
'compressionMethod' => $options->plugin('SwitchImges')->compressionMethod,
'compressext' => $options->plugin('SwitchImges')->compressext,
'gifHandling' => $options->plugin('SwitchImges')->gifHandling,
'backupOriginal' => $options->plugin('SwitchImges')->backupOriginal,
'vm_pic' => $options->plugin('SwitchImges')->vm_pic, // 确保这里的路径是正确的
'fmSize' => ($fmSize === '' || !isset($fmSize)) ? 10 : (int)$fmSize,
);
}
/**
* 上传文件处理函数
*
* @access public
* @param array $file 上传的文件
* @return mixed
*/
public static function uploadHandle($file)
{
if (empty($file['tmp_name'])) {
return false;
}
// 处理文件名和扩展名
$fileName = pathinfo($file['name'], PATHINFO_BASENAME);
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
// 获取设置
$settings = self::getSettings();
$uploadDir = __TYPECHO_ROOT_DIR__ . '/usr/uploads/';
$dateDir = date('Y/m/');
$fullUploadDir = $uploadDir . $dateDir;
// 如果不是支持的图片类型,使用默认处理
if (!self::checkFileType($ext)) {
return self::defaultUploadHandle($file);
}
// 如果上传的文件已经是目标格式(webp/avif),则只需要考虑是否添加水印
if ($ext === $settings['compressext']) {
// 使用bin2hex生成随机文件名,但保持原始扩展名
$uniqueFileName = bin2hex(random_bytes(16)) . '.' . $ext;
$outputFile = $fullUploadDir . $uniqueFileName;
// 确保目录存在
if (!file_exists($fullUploadDir)) {
mkdir($fullUploadDir, 0755, true);
}
// 获取文件大小
$fileSize = filesize($file['tmp_name']);
$fmSize = $settings['fmSize'] * 1024;
// 如果文件大小大于设定值,需要添加水印
if ($fileSize > $fmSize) {
// 创建临时文件
$tempFile = sys_get_temp_dir() . '/' . uniqid() . '.' . $ext;
move_uploaded_file($file['tmp_name'], $tempFile);
// 水印图片路径
$watermarkFile = __DIR__ . DIRECTORY_SEPARATOR . $settings['vm_pic'];
// 使用FFmpeg添加水印
$cmd = sprintf(
'ffmpeg -i %s -i %s -filter_complex "[0:v][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2" -q:v %d %s',
escapeshellarg($tempFile),
escapeshellarg($watermarkFile),
$settings['quality'],
escapeshellarg($outputFile)
);
exec($cmd, $output, $returnVar);
unlink($tempFile); // 删除临时文件
if ($returnVar !== 0) {
return false;
}
} else {
// 如果不需要水印,直接移动文件
if (!move_uploaded_file($file['tmp_name'], $outputFile)) {
return false;
}
}
// 返回文件信息
return array(
'name' => $fileName,
'path' => str_replace(__TYPECHO_ROOT_DIR__, '', $outputFile),
'size' => filesize($outputFile),
'type' => $ext,
'mime' => 'image/' . $ext
);
}
// 如果不是目标格式,则进行正常的转换流程
$uniqueFileName = bin2hex(random_bytes(16)) . '.' . $settings['compressext'];
$webpFilePath = $fullUploadDir . $uniqueFileName;
// 确保目录存在
if (!file_exists($fullUploadDir)) {
mkdir($fullUploadDir, 0755, true);
}
// 创建临时文件
$tempFilePath = sys_get_temp_dir() . '/' . uniqid() . '.' . $ext;
if (!move_uploaded_file($file['tmp_name'], $tempFilePath)) {
return false;
}
// 处理备份
if ($settings['backupOriginal'] === 'enable') {
$backupDir = $fullUploadDir . 'backup/';
if (!file_exists($backupDir)) {
mkdir($backupDir, 0755, true);
}
$backupFileName = pathinfo($uniqueFileName, PATHINFO_FILENAME);
$backupFilePath = $backupDir . $backupFileName . '_backup.' . $ext;
copy($tempFilePath, $backupFilePath);
}
// 根据设置选择转换方法
$conversionResult = false;
switch ($settings['compressionMethod']) {
case 'imagemagick':
$conversionResult = self::convertToWebPWithImageMagick($tempFilePath, $webpFilePath);
break;
case 'cwebp':
$conversionResult = self::convertToWebPWithCwebp($tempFilePath, $webpFilePath);
break;
case 'ffmpeg':
$conversionResult = self::convertToWebPWithFFmpeg($tempFilePath, $webpFilePath);
break;
default:
$conversionResult = self::convertToWebPWithGD($tempFilePath, $webpFilePath);
}
// 删除临时文件
unlink($tempFilePath);
if (!$conversionResult) {
return false;
}
return array(
'name' => $fileName,
'path' => str_replace(__TYPECHO_ROOT_DIR__, '', $webpFilePath),
'size' => filesize($webpFilePath),
'type' => $settings['compressext'],
'mime' => 'image/' . $settings['compressext']
);
}
/**
* 上传文件处理函数
*
* @access public
* @param array $file 上传的文件
* @return mixed
*/
public static function defaultUploadHandle($file)
{
if (empty($file['tmp_name'])) {
return false;
}
// 处理文件名和扩展名
$fileName = pathinfo($file['name'], PATHINFO_BASENAME);
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$uploadDir = __TYPECHO_ROOT_DIR__ . '/usr/uploads/';
$dateDir = date('Y/m/');
$fullUploadDir = $uploadDir . $dateDir;
// 使用 bin2hex 生成随机 32 个字符文件名
$uniqueFileName = bin2hex(random_bytes(16)) . '.' . $ext;
$webpFilePath = $fullUploadDir . $uniqueFileName;
//$webpFileName = uniqid() . '.'.$ext;
//$webpFilePath = $fullUploadDir . $webpFileName;
// 确保目录存在
if (!file_exists($fullUploadDir)) {
mkdir($fullUploadDir, 0755, true);
}
// 移动文件到临时路径
move_uploaded_file($file['tmp_name'], $webpFilePath);
// 返回 WebP 文件信息
return array(
'name' => $fileName,
'path' => str_replace(__TYPECHO_ROOT_DIR__, '', $webpFilePath),
'size' => filesize($webpFilePath),
'type' => $ext,
'mime' => $file['type']
);
}
/**
* 检查文件类型
*
* @access private
* @param string $ext 文件扩展名
* @return bool
*/
private static function checkFileType($ext)
{
$allowedExtensions = array('jpg', 'jpeg', 'png', 'gif', 'webp', 'avif');
return in_array($ext, $allowedExtensions);
}
/**
* 使用 FFmpeg 将图像转换为对应格式
*
* @access private
* @param string $sourceFile 源文件路径
* @param string $outputFile 输出文件路径
* @return bool 转换是否成功
*/
private static function convertToWebPWithFFmpeg($sourceFile, $outputFile)
{
// 获取设置
$settings = self::getSettings();
// 获取源文件扩展名
$ext = strtolower(pathinfo($sourceFile, PATHINFO_EXTENSION));
// 获取目标文件扩展名
$targetExt = $settings['compressext'];
// 获取源图像的宽度和高度
list($width, $height) = getimagesize($sourceFile);
// 计算等比缩放后的宽度和高度
$maxWidth = $settings['maxWidth'] ?? $width;
$maxHeight = $settings['maxHeight'] ?? $height;
$ratio = min($maxWidth / $width, $maxHeight / $height, 1);
$newWidth = $width * $ratio;
$newHeight = $height * $ratio;
// 水印路径
$vm_pic = $settings['vm_pic'];
$ffmpegmarkFile = __DIR__ . DIRECTORY_SEPARATOR . $vm_pic;
// 获取原文件大小
$fileSize = filesize($sourceFile);
$logFile = __DIR__ . '/ffmpeg_log.txt';
// 缩略图/封面体积阈值
$fmSize = $settings['fmSize'] * 1024;
// 检查是否已经是目标格式
$isTargetFormat = ($ext === $targetExt);
// 定义命令变量
$cmd = '';
// 根据文件大小和格式决定处理方式
if ($fileSize > $fmSize) {
// 需要添加水印
if (!file_exists($ffmpegmarkFile)) {
$logMessage = date('Y-m-d H:i:s') . " - 错误: 水印文件不存在: " . $ffmpegmarkFile . "\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
return false;
}
if ($isTargetFormat) {
// 已经是目标格式,只添加水印
$cmd = sprintf(
'ffmpeg -i %s -i %s -filter_complex "[0:v][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2" -q:v %d %s',
escapeshellarg($sourceFile),
escapeshellarg($ffmpegmarkFile),
$settings['quality'],
escapeshellarg($outputFile)
);
} else {
// 需要转换格式并添加水印
$cmd = sprintf(
'ffmpeg -i %s -i %s -filter_complex "[0:v][1:v]scale2ref=%d:%d[main][watermark];[main][watermark]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2" -q:v %d -loop 0 %s',
escapeshellarg($sourceFile),
escapeshellarg($ffmpegmarkFile),
$newWidth,
$newHeight,
$settings['quality'],
escapeshellarg($outputFile)
);
}
} else {
// 文件小于阈值,不需要添加水印
if ($isTargetFormat) {
// 已经是目标格式,直接复制文件
if (copy($sourceFile, $outputFile)) {
return true;
}
return false;
} else {
// 只需要转换格式
$cmd = sprintf(
'ffmpeg -i %s -vf "scale=%d:%d" -q:v %d -loop 0 %s',
escapeshellarg($sourceFile),
$newWidth,
$newHeight,
$settings['quality'],
escapeshellarg($outputFile)
);
}
}
// 如果有命令需要执行
if (!empty($cmd)) {
// 执行 FFmpeg 命令
exec($cmd, $output, $returnVar);
// 记录日志
$logMessage = "\n" .date('Y-m-d H:i:s') . " - 处理信息:\n";
$logMessage .= "源文件格式: {$ext}\n";
$logMessage .= "目标格式: {$targetExt}\n";
$logMessage .= "文件大小: {$fileSize} 字节\n";
$logMessage .= "大小阈值: {$fmSize} 字节\n";
$logMessage .= "是否添加水印: " . ($fileSize > $fmSize ? "是" : "否") . "\n";
$logMessage .= "是否需要转换格式: " . (!$isTargetFormat ? "是" : "否") . "\n";
if ($returnVar === 0) {
$logMessage .= "------------FFmpeg 执行成功\n";
} else {
$logMessage .= "------------FFmpeg 执行失败 错误信息: " . implode("\n", $output) . "\n";
}
file_put_contents($logFile, $logMessage, FILE_APPEND);
return $returnVar === 0;
}
return false;
}
/**
* 使用 GD 库转换为对应格式
*
* @access private
* @param string $sourceFile 源文件路径
* @param string $outputFile 输出文件路径
* @return bool 转换是否成功
*/
private static function convertToWebPWithGD($sourceFile, $outputFile)
{
// 获取设置,如最大宽度、高度及质量
$settings = self::getSettings();
// 获取源图像的宽度、高度和类型
list($width, $height, $type) = getimagesize($sourceFile);
// 计算等比缩放后的宽度和高度,确保不会超出最大宽高
$maxWidth = $settings['maxWidth'] ?? $width;
$maxHeight = $settings['maxHeight'] ?? $height;
$ratio = min($maxWidth / $width, $maxHeight / $height, 1);
$newWidth = $width * $ratio;
$newHeight = $height * $ratio;
// 根据图像类型选择相应的创建函数
$image = null;
switch ($type) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($sourceFile);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($sourceFile);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($sourceFile);
break;
default:
return false; // 不支持的图像类型
}
if (!$image) {
return false; // 图像创建失败
}
// 使用 GD 库对图像进行等比缩放
$resizedImage = imagescale($image, $newWidth, $newHeight);
// 生成 WebP 文件,使用指定的质量参数
$result = imagewebp($resizedImage, $outputFile, $settings['quality']);
// 销毁图像资源,释放内存
imagedestroy($image);
imagedestroy($resizedImage);
return $result;
}
/**
* 使用 ImageMagick 转换为对应格式
*
* @access private
* @param string $sourceFile 源文件路径
* @param string $outputFile 输出文件路径
* @return bool 转换是否成功
*/
private static function convertToWebPWithImageMagick($sourceFile, $outputFile)
{
// 获取设置,如最大宽度、高度及质量
$settings = self::getSettings();
// 获取源图像的宽度和高度
list($width, $height) = getimagesize($sourceFile);
// 计算等比缩放后的宽度和高度,确保不会超出最大宽高
$maxWidth = $settings['maxWidth'] ?? $width;
$maxHeight = $settings['maxHeight'] ?? $height;
$ratio = min($maxWidth / $width, $maxHeight / $height, 1);
$newWidth = $width * $ratio;
$newHeight = $height * $ratio;
// 使用 ImageMagick 命令行工具进行转换并缩放
$cmd = sprintf(
'convert %s -resize %dx%d -compress lzw -quality %d %s',
escapeshellarg($sourceFile),
$newWidth,
$newHeight,
$settings['quality'],
escapeshellarg($outputFile)
);
// 执行命令
exec($cmd, $output, $returnVar);
return $returnVar === 0;
}
}
?>