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 ''; } }