uploadHandle = array('Qiniu_Plugin', 'uploadHandle');
Typecho_Plugin::factory('Widget_Upload')->modifyHandle = array('Qiniu_Plugin', 'modifyHandle');
Typecho_Plugin::factory('Widget_Upload')->deleteHandle = array('Qiniu_Plugin', 'deleteHandle');
Typecho_Plugin::factory('Widget_Upload')->attachmentHandle = array('Qiniu_Plugin', 'attachmentHandle');
// 添加文章内容输出时的EXIF信息显示
Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array('Qiniu_Plugin', 'parseContent');
Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('Qiniu_Plugin', 'parseContent');
// 使用安全的CSS加载方式
Typecho_Plugin::factory('Widget_Archive')->footer = array('Qiniu_Plugin', 'footer');
return _t('插件已激活,请先配置七牛云信息!');
}
// 禁用插件
public static function deactivate()
{
return _t('插件已禁用');
}
// 插件配置面板
public static function config(Typecho_Widget_Helper_Form $form)
{
$bucket = new Typecho_Widget_Helper_Form_Element_Text('bucket', null, null, _t('空间名称:'), _t('七牛云存储空间名称'));
$bucket->addRule('required', _t('"空间名称"不能为空!'));
$form->addInput($bucket);
$accesskey = new Typecho_Widget_Helper_Form_Element_Text('accesskey', null, null, _t('AccessKey:'), _t('从七牛云控制台获取'));
$form->addInput($accesskey->addRule('required', _t('AccessKey不能为空!')));
$secretkey = new Typecho_Widget_Helper_Form_Element_Text('secretkey', null, null, _t('SecretKey:'), _t('从七牛云控制台获取'));
$form->addInput($secretkey->addRule('required', _t('SecretKey不能为空!')));
$domain = new Typecho_Widget_Helper_Form_Element_Text('domain', null, 'https://', _t('绑定域名:'), _t('空间绑定的域名,如:https://cdn.example.com'));
$form->addInput($domain->addRule('required', _t('请填写空间绑定的域名!'))->addRule('url', _t('您输入的域名格式错误!')));
$savepath = new Typecho_Widget_Helper_Form_Element_Text('savepath', null, 'typecho/{year}/{month}/', _t('保存路径前缀'), _t('可使用变量:{year}, {month}, {day}, {random}'));
$form->addInput($savepath);
// WebP转换开关
$webp = new Typecho_Widget_Helper_Form_Element_Radio('webp',
array('0' => _t('关闭'), '1' => _t('开启')),
'0',
_t('WebP自动转换:'),
_t('开启后上传的JPEG/PNG图片将自动转换为WebP格式'));
$form->addInput($webp);
// 图片质量设置
$quality = new Typecho_Widget_Helper_Form_Element_Text('quality', null, '85', _t('图片质量:'),
_t('设置图片压缩质量(1-100),仅对JPEG/PNG/WEBP格式有效。85为推荐值'));
$quality->addRule('required', _t('图片质量不能为空!'))
->addRule('isInteger', _t('必须输入数字!'))
->addRule('range', _t('请输入1-100之间的数字!'), array(1, 100));
$form->addInput($quality);
// EXIF信息开关
$exif = new Typecho_Widget_Helper_Form_Element_Radio('exif',
array('0' => _t('关闭'), '1' => _t('开启')),
'0',
_t('EXIF信息显示:'),
_t('开启后文章图片悬停时会显示拍摄信息(相机型号、光圈、快门等)'));
$form->addInput($exif);
}
// 个人用户配置面板
public static function personalConfig(Typecho_Widget_Helper_Form $form)
{
}
// 获得插件配置信息
public static function getConfig()
{
return Typecho_Widget::widget('Widget_Options')->plugin('Qiniu');
}
// 安全的CSS加载方式
public static function footer()
{
$option = self::getConfig();
if ($option->exif && !self::$_cssLoaded) {
// 只在文章页面加载CSS
$widget = Typecho_Widget::widget('Widget_Archive');
if ($widget->is('single')) {
echo '';
self::$_cssLoaded = true;
}
}
}
// 输出CSS内容
private static function echoCss()
{
echo '.qiniu-exif-container {
display: inline-block;
position: relative;
max-width: 100%;
}
.qiniu-exif-info {
position: absolute;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.85);
color: white;
padding: 8px 10px;
font-size: 12px;
line-height: 1.4;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 9999;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
pointer-events: none;
}
.qiniu-exif-container:hover .qiniu-exif-info {
opacity: 1;
visibility: visible;
}
.qiniu-exif-info div {
margin: 3px 0;
}
.qiniu-exif-info strong {
font-weight: 600;
color: #fff;
}';
}
/**
* 解析文章内容,为图片添加EXIF容器
*/
public static function parseContent($content, $widget, $lastResult)
{
$content = $lastResult ? $lastResult : $content;
$option = self::getConfig();
if (!$option->exif || !$widget->is('single')) {
return $content;
}
$pattern = '/
]*src=["\']([^"\']+)["\'][^>]*>/i';
return preg_replace_callback($pattern, function($matches) {
$imgTag = $matches[0];
$src = $matches[1];
$option = self::getConfig();
$domain = rtrim($option->domain, '/');
// 检查是否是七牛云的图片
if (strpos($src, $domain) === false) {
return $imgTag;
}
// 从URL中提取文件路径
$path = str_replace($domain, '', $src);
$path = ltrim(parse_url($path, PHP_URL_PATH), '/');
// 去除图片处理参数
if (strpos($path, '?') !== false) {
$path = substr($path, 0, strpos($path, '?'));
}
// 从数据库获取EXIF信息
$db = Typecho_Db::get();
$row = $db->fetchRow($db->select()->from('table.contents')
->where('type = ?', 'attachment')
->where('text LIKE ?', '%"path":"' . $path . '"%')
->limit(1));
if ($row) {
$attachment = unserialize($row['text']);
if (isset($attachment['exif']) && !empty($attachment['exif'])) {
$exifData = $attachment['exif'];
// 生成简化的EXIF信息HTML
$exifHtml = '
';
if (!empty($exifData['camera'])) {
$exifHtml .= '
' . htmlspecialchars($exifData['camera']) . '
';
}
$details = array();
if (!empty($exifData['exposure'])) {
$details[] = '快门:' . htmlspecialchars($exifData['exposure']);
}
if (!empty($exifData['aperture'])) {
$details[] = '光圈:f/' . htmlspecialchars($exifData['aperture']);
}
if (!empty($exifData['iso'])) {
$details[] = 'ISO:' . htmlspecialchars($exifData['iso']);
}
if (!empty($exifData['focal_length'])) {
$details[] = '焦距:' . htmlspecialchars($exifData['focal_length']) . 'mm';
}
if (!empty($details)) {
$exifHtml .= '
' . implode(' ', $details) . '
';
}
$exifHtml .= '
';
return '' . $imgTag . $exifHtml . '
';
}
}
return $imgTag;
}, $content);
}
/**
* 提取EXIF信息
*/
private static function extractExif($filePath)
{
if (!function_exists('exif_read_data')) {
return array();
}
$exif = @exif_read_data($filePath);
if (!$exif) {
return array();
}
$result = array();
// 相机型号
if (!empty($exif['Make']) || !empty($exif['Model'])) {
$make = !empty($exif['Make']) ? trim($exif['Make']) : '';
$model = !empty($exif['Model']) ? trim($exif['Model']) : '';
$result['camera'] = trim($make . ' ' . $model);
}
// 曝光时间
if (!empty($exif['ExposureTime'])) {
$result['exposure'] = $exif['ExposureTime'];
}
// 光圈值
if (!empty($exif['FNumber'])) {
$fnumber = is_array($exif['FNumber']) ? $exif['FNumber'][0] / $exif['FNumber'][1] : $exif['FNumber'];
$result['aperture'] = number_format($fnumber, 1);
}
// ISO
if (!empty($exif['ISOSpeedRatings'])) {
$result['iso'] = $exif['ISOSpeedRatings'];
}
// 焦距
if (!empty($exif['FocalLength'])) {
if (is_array($exif['FocalLength'])) {
$focal = $exif['FocalLength'][0] / $exif['FocalLength'][1];
} else {
$focal = $exif['FocalLength'];
}
$result['focal_length'] = number_format($focal, 1);
}
// 拍摄时间
if (!empty($exif['DateTimeOriginal'])) {
$result['date'] = date('Y-m-d H:i', strtotime($exif['DateTimeOriginal']));
}
return $result;
}
/**
* 检测是否支持WebP转换
*/
private static function canConvertWebp()
{
if (!extension_loaded('gd')) {
return false;
}
$gdInfo = gd_info();
return isset($gdInfo['WebP Support']) && $gdInfo['WebP Support'];
}
/**
* 压缩图片并转换格式
*/
private static function compressAndConvert($sourcePath, $targetExt, $quality)
{
$imageInfo = @getimagesize($sourcePath);
if (!$imageInfo) {
return $sourcePath;
}
$mime = $imageInfo['mime'];
// 创建图像资源
switch ($mime) {
case 'image/jpeg':
$image = imagecreatefromjpeg($sourcePath);
break;
case 'image/png':
$image = imagecreatefrompng($sourcePath);
imagepalettetotruecolor($image);
imagealphablending($image, false);
imagesavealpha($image, true);
break;
case 'image/gif':
$image = imagecreatefromgif($sourcePath);
break;
default:
return $sourcePath;
}
if (!$image) {
return $sourcePath;
}
$tempFile = tempnam(sys_get_temp_dir(), 'img_') . '.' . $targetExt;
$success = false;
switch ($targetExt) {
case 'jpg':
case 'jpeg':
$success = imagejpeg($image, $tempFile, $quality);
break;
case 'png':
$pngQuality = 9 - round(($quality / 100) * 9);
$success = imagepng($image, $tempFile, $pngQuality);
break;
case 'webp':
if (self::canConvertWebp()) {
$success = imagewebp($image, $tempFile, $quality);
}
break;
}
imagedestroy($image);
if ($success && file_exists($tempFile)) {
return $tempFile;
}
return $sourcePath;
}
/**
* 使用七牛基本图片处理
*/
private static function applyQiniuStyle($url, $option)
{
$pathInfo = pathinfo($url);
$currentExt = isset($pathInfo['extension']) ? strtolower($pathInfo['extension']) : '';
$needQuality = $option->quality && $option->quality != 85;
$needWebP = $option->webp && $currentExt != 'webp';
if (!$needQuality && !$needWebP) {
return $url;
}
$params = array();
if ($needQuality) {
$params[] = 'q/' . intval($option->quality);
}
if ($needWebP) {
$params[] = 'format/webp';
}
if (!empty($params)) {
$separator = (strpos($url, '?') === false) ? '?' : '&';
$url .= $separator . 'imageView2/2/' . implode('/', $params);
}
return $url;
}
/**
* 删除文件
*/
public static function deleteFile($filepath)
{
try {
$option = self::getConfig();
$auth = new Auth($option->accesskey, $option->secretkey);
$bucketMgr = new BucketManager($auth);
$err = $bucketMgr->delete($option->bucket, $filepath);
return $err === null;
} catch (Exception $e) {
return false;
}
}
/**
* 上传文件到七牛云 - 修复中文文件名问题
*/
public static function uploadFile($file, $content = null)
{
error_reporting(0);
if (empty($file['name']) || !isset($file['tmp_name'])) {
return array('error' => 1, 'message' => '上传文件无效');
}
// 获取原始文件名
$originalName = basename($file['name']);
// 使用pathinfo正确处理中文字符
$pathInfo = pathinfo($originalName);
$originalExt = isset($pathInfo['extension']) ? strtolower($pathInfo['extension']) : '';
$baseName = isset($pathInfo['filename']) ? $pathInfo['filename'] : '';
// 校验扩展名
if (!Widget_Upload::checkFileType($originalExt)) {
return array('error' => 1, 'message' => '不允许上传此类型文件');
}
$option = self::getConfig();
if (empty($option->bucket) || empty($option->accesskey) || empty($option->secretkey) || empty($option->domain)) {
return array('error' => 1, 'message' => '请先正确配置七牛云插件');
}
$date = new Typecho_Date(Typecho_Widget::widget('Widget_Options')->gmtTime);
// 提取EXIF信息
$exifData = array();
if ($option->exif && in_array($originalExt, array('jpg', 'jpeg'))) {
$exifData = self::extractExif($file['tmp_name']);
}
// 处理图片压缩和转换
$tempFile = null;
$uploadPath = $file['tmp_name'];
$finalExt = $originalExt;
$supportedImages = array('jpg', 'jpeg', 'png', 'gif');
if (in_array($originalExt, $supportedImages)) {
$quality = isset($option->quality) ? intval($option->quality) : 85;
$quality = max(1, min(100, $quality));
$targetExt = $originalExt;
if ($option->webp && self::canConvertWebp() && $originalExt != 'gif') {
$targetExt = 'webp';
}
if ($targetExt != $originalExt || $quality != 85) {
$processedPath = self::compressAndConvert($file['tmp_name'], $targetExt, $quality);
if ($processedPath != $file['tmp_name']) {
$uploadPath = $processedPath;
$finalExt = $targetExt;
$tempFile = $processedPath;
}
}
}
// 生成存储路径 - 保持原始文件名,只替换文件系统不允许的字符
// 处理文件名,保留中文字符,只替换特殊字符
$safeName = $baseName;
// 替换文件系统不允许的字符,但保留中文字符
$safeName = preg_replace('/[<>:"\/\\|?*]/', '_', $safeName);
// 移除首尾空格
$safeName = trim($safeName);
// 如果名称为空,使用时间戳
if (empty($safeName)) {
$safeName = 'image_' . time();
}
if (isset($content)) {
$filePath = $content['attachment']->path;
self::deleteFile($filePath);
} else {
$savepath = preg_replace(array('/\{year\}/', '/\{month\}/', '/\{day\}/', '/\{random\}/'),
array($date->year, $date->month, $date->day, uniqid()),
$option->savepath);
$filePath = rtrim($savepath, '/') . '/' . $safeName . '.' . $finalExt;
}
if (!file_exists($uploadPath)) {
return array('error' => 1, 'message' => '上传文件不存在');
}
try {
$upManager = new UploadManager();
$auth = new Auth($option->accesskey, $option->secretkey);
$token = $auth->uploadToken($option->bucket);
// 七牛云SDK会自动处理文件名编码
list($ret, $error) = $upManager->putFile($token, $filePath, $uploadPath);
if ($tempFile && file_exists($tempFile) && $tempFile != $file['tmp_name']) {
@unlink($tempFile);
}
if ($error == null) {
$rawUrl = Typecho_Common::url($filePath, $option->domain);
$processedUrl = self::applyQiniuStyle($rawUrl, $option);
$fileSize = @filesize($uploadPath);
$result = array(
'name' => $originalName, // 使用原始文件名
'path' => $filePath,
'size' => $fileSize ? (int)$fileSize : 0,
'type' => $finalExt,
'mime' => $finalExt == 'webp' ? 'image/webp' : Typecho_Common::mimeContentType($uploadPath),
'url' => $processedUrl
);
if (!empty($exifData)) {
$result['exif'] = $exifData;
}
return $result;
} else {
return array('error' => 1, 'message' => '七牛云上传失败: ' . $error->message());
}
} catch (Exception $e) {
if ($tempFile && file_exists($tempFile) && $tempFile != $file['tmp_name']) {
@unlink($tempFile);
}
return array('error' => 1, 'message' => '上传异常: ' . $e->getMessage());
}
}
// 上传文件处理函数
public static function uploadHandle($file)
{
return self::uploadFile($file);
}
// 修改文件处理函数
public static function modifyHandle($content, $file)
{
return self::uploadFile($file, $content);
}
// 删除文件处理函数
public static function deleteHandle(array $content)
{
if (isset($content['attachment'])) {
self::deleteFile($content['attachment']->path);
}
}
// 获取实际文件绝对访问路径
public static function attachmentHandle(array $content)
{
$option = self::getConfig();
if (isset($content['attachment'])) {
$rawUrl = Typecho_Common::url($content['attachment']->path, $option->domain);
return self::applyQiniuStyle($rawUrl, $option);
}
return '';
}
}