在很多应用场景中,用户上传的图片或视频需要打上专属的水印(如版权声明、拍摄地点、时间等),以防止盗用并标明出处。本文将基于 ThinkPHP5(FastAdmin)框架,演示如何利用 阿里云OSS(对象存储) 以及 MPS(媒体处理服务) ,实现图片和视频在上传后自动添加文字水印的完整方案。
一、 业务场景与需求
1. 核心需求
- 多端支持 :客户端(App/小程序/H5)通过接口将文件上传至服务端。
- 自适应判断 :服务端根据上传的文件类型,自动判断是图片还是视频。
- 图片处理(OSS) :如果是图片,直接利用阿里云 OSS 的图片处理能力,拼接 URL 参数( x-oss-process ),实现 实时 图文水印。
- 视频处理(MPS) :如果是视频,由于视频水印无法实时渲染,需要调用阿里云 MPS 媒体处理服务,提交一个 异步转码任务 。在原视频的基础上烧录文字水印,并生成一个新的带水印的 MP4 文件。
- 自定义参数 :支持前端传入自定义的水印文字(支持两行,如地点+时间)、颜色、大小以及显示位置(左上、右上、左下、右下)。
2. 阿里云前置配置
在实现代码前,需要确保你已经开通了以下阿里云服务,并获取了关键参数:
- AccessKey 权限 :需要一个拥有 AliyunOSSFullAccess 和 AliyunMTSFullAccess 权限的子账号 AccessKey。
- MPS 管道 ID ( PipelineId ) :进入 MPS 控制台 -> 全局设置 -> 管道及回调,创建一个处理队列,拿到 32 位的 PipelineId。
- MPS 转码模板 ID ( TemplateId ) :可以使用系统预置模板(例如输出标准 MP4 格式的 S00000001-200020 ),也可以自定义模板。
二、 完整核心代码实现
以下是在 ThinkPHP5 API 控制器中的完整代码实现,包含了文件上传、图片水印参数组装以及 MPS 视频水印转码任务的提交逻辑。
php
<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\exception\UploadException;
use app\common\library\Upload;
use OSS\OssClient;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use think\Log;
class OssUpload extends Api
{
protected $noNeedLogin = ['upload'];
protected $noNeedRight = ['*'];
/**
* 上传文件并添加文字水印(图片实时处理 / 视频MPS异步转码)
*/
public function upload()
{
// 1. 接收前端传入的水印参数
$watermark_text = $this->request->post('watermark_text', '');
$watermark_address = $this->request->post('watermark_address', '');
$watermark_time = $this->request->post('watermark_time', '');
$watermark_size = 30; // 固定或接收传入
$watermark_color = $this->request->post('watermark_color', 'FF0000');
$watermark_color = strtoupper(ltrim($watermark_color, '#')); // 去除#号
$watermark_position = $this->request->post('watermark_position', 'right_bottom');
$video_watermark_address = $watermark_address ?: $watermark_text;
$video_watermark_time = $watermark_time;
// 获取 FastAdmin 插件配置中的 OSS 参数
$config = get_addon_config('alioss');
if (!$config) {
$this->error('未配置或未安装阿里云OSS插件');
}
try {
$oss = new OssClient($config['accessKeyId'], $config['accessKeySecret'], $config['endpoint']);
} catch (\Exception $e) {
$this->error('OSS客户端初始化失败');
}
// 2. 处理本地文件上传
$file = $this->request->file('file');
if (!$file) {
$this->error('未上传文件');
}
try {
$upload = new Upload($file);
$attachment = $upload->upload();
} catch (UploadException $e) {
$this->error($e->getMessage());
}
$filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
$url = $attachment->url;
$objectKey = ltrim($url, "/");
// 3. 将本地文件推送到阿里云 OSS
try {
$oss->uploadFile($config['bucket'], $objectKey, $filePath);
} catch (\Exception $e) {
Log::record('文件推送到OSS失败: ' . $e->getMessage(), 'error');
$this->error("上传失败:" . $e->getMessage());
}
$fullurl = cdnurl($url, true);
$isImage = in_array(strtolower($attachment->imagetype), ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']);
$isVideo = in_array(strtolower($attachment->imagetype), ['mp4', 'avi', 'flv', 'mov', 'mkv']);
// ==========================================
// 场景 A:处理图片水印(实时 URL 拼接)
// ==========================================
if ($isImage) {
$imageWatermarkParams = [];
// 九宫格定位映射
$imagePositionMap = [
'left_top' => 'nw', 'right_top' => 'ne',
'left_bottom' => 'sw', 'right_bottom' => 'se',
];
$g_pos = isset($imagePositionMap[$watermark_position]) ? $imagePositionMap[$watermark_position] : 'sw';
if ($watermark_address || $watermark_time) {
// 注意:图片水印的文字必须进行 URL 安全的 Base64 编码
if ($watermark_address) {
$encodedAddress = rtrim(strtr(base64_encode($watermark_address), '+/', '-_'), '=');
$imageWatermarkParams[] = 'watermark,text_' . $encodedAddress . ',color_' . $watermark_color . ',size_' . $watermark_size . ',g_' . $g_pos . ',x_10,y_40,t_90';
}
if ($watermark_time) {
$encodedTime = rtrim(strtr(base64_encode($watermark_time), '+/', '-_'), '=');
$imageWatermarkParams[] = 'watermark,text_' . $encodedTime . ',color_' . $watermark_color . ',size_26,g_' . $g_pos . ',x_10,y_10,t_90';
}
}
// 拼接 OSS 图片处理参数
if (!empty($imageWatermarkParams)) {
$watermarkParam = '?x-oss-process=image/' . implode('/', $imageWatermarkParams);
$url .= $watermarkParam;
$fullurl .= $watermarkParam;
}
}
// ==========================================
// 场景 B:处理视频水印(提交 MPS 转码任务)
// ==========================================
if (($video_watermark_address || $video_watermark_time) && $isVideo) {
$pipelineId = '7beb974xxxxxx'; // 替换为你的管道 ID
// 提取区域 ID,例如 cn-shanghai
$regionId = 'cn-shanghai';
if (preg_match('/oss-(.*?)\./', $config['endpoint'], $matches)) {
$regionId = $matches[1];
}
$location = 'oss-' . $regionId;
// 定义带水印输出的新文件名
$newObjectKey = str_replace('.' . $attachment->imagetype, '_watermark.mp4', $objectKey);
$newUrl = '/' . $newObjectKey;
$newFullurl = cdnurl($newUrl, true);
// MPS 要求颜色为英文单词
$videoFontColorMap = [
'FFFFFF' => 'White', '000000' => 'Black', 'FF0000' => 'Red',
];
$videoFontColor = isset($videoFontColorMap[$watermark_color]) ? $videoFontColorMap[$watermark_color] : 'Red';
// 视频水印位置使用相对百分比定位
$videoWatermarkPositionMap = [
'left_bottom' => [
'address' => ['Top' => '0.76', 'Left' => '0.03'],
'time' => ['Top' => '0.84', 'Left' => '0.03'],
],
'right_bottom' => [
'address' => ['Top' => '0.76', 'Left' => '0.58'],
'time' => ['Top' => '0.84', 'Left' => '0.58'],
],
];
$videoWatermarkPosition = isset($videoWatermarkPositionMap[$watermark_position]) ? $videoWatermarkPositionMap[$watermark_position] : $videoWatermarkPositionMap['right_bottom'];
// 组装水印数组
$videoWaterMarks = [];
if ($video_watermark_address) {
$videoWaterMarks[] = [
'Type' => 'Text',
'TextWaterMark' => [
'Content' => base64_encode($video_watermark_address), // 视频水印普通 Base64 即可
'FontName' => 'SimSun',
'FontSize' => $watermark_size,
'FontColor' => $videoFontColor,
'FontAlpha' => 1,
'Top' => $videoWatermarkPosition['address']['Top'],
'Left' => $videoWatermarkPosition['address']['Left']
]
];
}
try {
// 初始化 MPS 客户端
AlibabaCloud::accessKeyClient($config['accessKeyId'], $config['accessKeySecret'])
->regionId($regionId)
->asDefaultClient();
$outputs = [
[
'OutputObject' => urlencode($newObjectKey), // 文件名必须 urlencode
'TemplateId' => 'S00000001-200020',
'WaterMarks' => $videoWaterMarks
]
];
// 提交 MPS 转码作业 (SubmitJobs)
$result = AlibabaCloud::rpc()
->product('Mts')
->version('2014-06-18')
->action('SubmitJobs')
->method('POST')
->host("mts.{$regionId}.aliyuncs.com")
->options([
'query' => [
'PipelineId' => $pipelineId,
'Input' => json_encode(['Bucket' => $config['bucket'], 'Location' => $location, 'Object' => urlencode($objectKey)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'OutputBucket' => $config['bucket'],
'OutputLocation' => $location,
'Outputs' => json_encode($outputs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
],
])
->request();
Log::record('MPS任务提交成功: ' . json_encode($result->toArray(), JSON_UNESCAPED_UNICODE), 'info');
// 替换 URL 为最终水印视频的 URL
$url = $newUrl;
$fullurl = $newFullurl;
// 更新数据库中的记录
$attachment->url = $url;
$attachment->save();
} catch (\Exception $e) {
Log::record('MPS异常: ' . $e->getMessage(), 'error');
}
}
$this->success("上传成功", ['url' => $url, 'fullurl' => $fullurl]);
}
}