1244 lines
51 KiB
PHP
1244 lines
51 KiB
PHP
<?php
|
||
/**
|
||
* 文章挂图片标题、水印
|
||
*
|
||
* @package ImageCaptionWatermark
|
||
* @author 石头厝
|
||
* @version 3.5
|
||
* @link https://www.shitoucuo.com/
|
||
*/
|
||
|
||
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
||
|
||
class ImageCaptionWatermark_Plugin implements Typecho_Plugin_Interface
|
||
{
|
||
/**
|
||
* 激活插件
|
||
*/
|
||
public static function activate()
|
||
{
|
||
Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array('ImageCaptionWatermark_Plugin', 'parseContent');
|
||
Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('ImageCaptionWatermark_Plugin', 'parseContent');
|
||
Typecho_Plugin::factory('Widget_Archive')->footer = array('ImageCaptionWatermark_Plugin', 'addFooter');
|
||
|
||
return '插件已激活,请到设置页面配置参数。';
|
||
}
|
||
|
||
/**
|
||
* 禁用插件
|
||
*/
|
||
public static function deactivate()
|
||
{
|
||
return '插件已禁用';
|
||
}
|
||
|
||
/**
|
||
* 插件配置面板
|
||
*/
|
||
public static function config(Typecho_Widget_Helper_Form $form)
|
||
{
|
||
// 标题设置部分
|
||
$titleEnable = new Typecho_Widget_Helper_Form_Element_Radio(
|
||
'titleEnable',
|
||
array('1' => '开启', '0' => '关闭'),
|
||
'1',
|
||
'<strong>启用图片标题</strong>',
|
||
'是否自动为文章中的图片添加标题'
|
||
);
|
||
$form->addInput($titleEnable);
|
||
|
||
$titlePosition = new Typecho_Widget_Helper_Form_Element_Select(
|
||
'titlePosition',
|
||
array(
|
||
'bottom-center' => '底部居中',
|
||
'bottom-right' => '右下角',
|
||
'bottom-left' => '左下角',
|
||
'top-center' => '顶部居中',
|
||
'top-right' => '右上角',
|
||
'top-left' => '左上角'
|
||
),
|
||
'bottom-center',
|
||
'标题显示位置',
|
||
'标题在图片上的显示位置'
|
||
);
|
||
$form->addInput($titlePosition);
|
||
|
||
$titleMinWidth = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titleMinWidth',
|
||
NULL,
|
||
'300',
|
||
'标题最小图片宽度',
|
||
'页面显示宽度大于此值(像素)的图片才显示标题'
|
||
);
|
||
$titleMinWidth->input->setAttribute('style', 'width: 120px;');
|
||
$form->addInput($titleMinWidth->addRule('isInteger', '请输入整数'));
|
||
|
||
// 新增:标题最小高度
|
||
$titleMinHeight = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titleMinHeight',
|
||
NULL,
|
||
'300',
|
||
'标题最小图片高度',
|
||
'页面显示高度大于此值(像素)的图片才显示标题'
|
||
);
|
||
$titleMinHeight->input->setAttribute('style', 'width: 120px;');
|
||
$form->addInput($titleMinHeight->addRule('isInteger', '请输入整数'));
|
||
|
||
$titleChineseOnly = new Typecho_Widget_Helper_Form_Element_Radio(
|
||
'titleChineseOnly',
|
||
array('1' => '开启', '0' => '关闭'),
|
||
'1',
|
||
'仅显示中文标题',
|
||
'开启:如果图片alt/title中没有中文,则不显示标题<br>关闭:显示所有标题'
|
||
);
|
||
$form->addInput($titleChineseOnly);
|
||
|
||
$titleColor = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titleColor',
|
||
NULL,
|
||
'#FFFFFF',
|
||
'标题文字颜色',
|
||
'标题文字的颜色'
|
||
);
|
||
$titleColor->input->setAttribute('style', 'width: 150px;');
|
||
$form->addInput($titleColor);
|
||
|
||
$titleBgColor = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titleBgColor',
|
||
NULL,
|
||
'rgba(0,0,0,0.85)',
|
||
'标题背景颜色',
|
||
'标题背景的颜色'
|
||
);
|
||
$titleBgColor->input->setAttribute('style', 'width: 150px;');
|
||
$form->addInput($titleBgColor);
|
||
|
||
// 新增:标题背景透明度
|
||
$titleBgOpacity = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titleBgOpacity',
|
||
NULL,
|
||
'0.85',
|
||
'标题背景透明度',
|
||
'0-1之间,1为不透明'
|
||
);
|
||
$titleBgOpacity->input->setAttribute('style', 'width: 100px;');
|
||
$titleBgOpacity->input->setAttribute('placeholder', '0.0-1.0');
|
||
$form->addInput($titleBgOpacity);
|
||
|
||
// 修改:标题显示分类
|
||
$titleIncludeCategories = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titleIncludeCategories',
|
||
NULL,
|
||
'',
|
||
'标题显示分类',
|
||
'仅在此分类下的文章显示图片标题,留空表示所有分类都显示<br>多个分类用逗号间隔,如:技术,生活'
|
||
);
|
||
$titleIncludeCategories->input->setAttribute('style', 'width: 300px;');
|
||
$form->addInput($titleIncludeCategories);
|
||
|
||
$titleFontSize = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titleFontSize',
|
||
NULL,
|
||
'14',
|
||
'标题字体大小',
|
||
'标题文字的大小(像素)'
|
||
);
|
||
$titleFontSize->input->setAttribute('style', 'width: 120px;');
|
||
$form->addInput($titleFontSize->addRule('isInteger', '请输入整数'));
|
||
|
||
$titlePadding = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'titlePadding',
|
||
NULL,
|
||
'8px 15px',
|
||
'标题内边距',
|
||
'标题的内边距,格式:上下 左右'
|
||
);
|
||
$titlePadding->input->setAttribute('style', 'width: 150px;');
|
||
$form->addInput($titlePadding);
|
||
|
||
// 新增:是否跳过横排组内的图片标题
|
||
$skipFlexGroupTitles = new Typecho_Widget_Helper_Form_Element_Radio(
|
||
'skipFlexGroupTitles',
|
||
array('1' => '跳过', '0' => '不跳过'),
|
||
'1',
|
||
'跳过横排组内的图片标题',
|
||
'开启:在横排图片组(flex-group)中不显示单张图片的标题,只显示组标题<br>关闭:同时显示组标题和单图标题'
|
||
);
|
||
$form->addInput($skipFlexGroupTitles);
|
||
|
||
// 水印设置部分
|
||
$watermarkEnable = new Typecho_Widget_Helper_Form_Element_Radio(
|
||
'watermarkEnable',
|
||
array('1' => '开启', '0' => '关闭'),
|
||
'1',
|
||
'<strong>启用图片水印</strong>',
|
||
'是否自动为文章中的图片添加水印'
|
||
);
|
||
$form->addInput($watermarkEnable);
|
||
|
||
$watermarkText = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'watermarkText',
|
||
NULL,
|
||
'SHITOUCUO.COM',
|
||
'水印文字内容',
|
||
'水印文字的内容'
|
||
);
|
||
$watermarkText->input->setAttribute('style', 'width: 300px;');
|
||
$form->addInput($watermarkText);
|
||
|
||
$watermarkPosition = new Typecho_Widget_Helper_Form_Element_Select(
|
||
'watermarkPosition',
|
||
array(
|
||
'top-left' => '左上角',
|
||
'top-center' => '顶部居中',
|
||
'top-right' => '右上角',
|
||
'center' => '正中央',
|
||
'bottom-left' => '左下角',
|
||
'bottom-center' => '底部居中',
|
||
'bottom-right' => '右下角'
|
||
),
|
||
'top-center',
|
||
'水印显示位置',
|
||
'避免与标题重叠,建议使用顶部位置'
|
||
);
|
||
$form->addInput($watermarkPosition);
|
||
|
||
$watermarkMinWidth = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'watermarkMinWidth',
|
||
NULL,
|
||
'200',
|
||
'水印最小图片宽度',
|
||
'页面显示宽度大于此值(像素)的图片才添加水印'
|
||
);
|
||
$watermarkMinWidth->input->setAttribute('style', 'width: 120px;');
|
||
$form->addInput($watermarkMinWidth->addRule('isInteger', '请输入整数'));
|
||
|
||
// 新增:水印最小高度
|
||
$watermarkMinHeight = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'watermarkMinHeight',
|
||
NULL,
|
||
'500',
|
||
'水印最小图片高度',
|
||
'页面显示高度大于此值(像素)的图片才添加水印'
|
||
);
|
||
$watermarkMinHeight->input->setAttribute('style', 'width: 120px;');
|
||
$form->addInput($watermarkMinHeight->addRule('isInteger', '请输入整数'));
|
||
|
||
// 修改:水印显示分类
|
||
$watermarkIncludeCategories = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'watermarkIncludeCategories',
|
||
NULL,
|
||
'',
|
||
'水印显示分类',
|
||
'仅在此分类下的文章显示图片水印,留空表示所有分类都显示<br>多个分类用逗号间隔,如:技术,生活'
|
||
);
|
||
$watermarkIncludeCategories->input->setAttribute('style', 'width: 300px;');
|
||
$form->addInput($watermarkIncludeCategories);
|
||
|
||
$watermarkOpacity = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'watermarkOpacity',
|
||
NULL,
|
||
'0.7',
|
||
'水印透明度',
|
||
'0-1之间,1为不透明'
|
||
);
|
||
$watermarkOpacity->input->setAttribute('style', 'width: 100px;');
|
||
$watermarkOpacity->input->setAttribute('placeholder', '0.0-1.0');
|
||
$form->addInput($watermarkOpacity);
|
||
|
||
$watermarkFontSize = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'watermarkFontSize',
|
||
NULL,
|
||
'24',
|
||
'水印字体大小',
|
||
'文字水印的字体大小(像素)'
|
||
);
|
||
$watermarkFontSize->input->setAttribute('style', 'width: 100px;');
|
||
$form->addInput($watermarkFontSize->addRule('isInteger', '请输入整数'));
|
||
|
||
$watermarkFontColor = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'watermarkFontColor',
|
||
NULL,
|
||
'rgba(255,255,255,0.9)',
|
||
'水印文字颜色',
|
||
'文字水印的颜色'
|
||
);
|
||
$watermarkFontColor->input->setAttribute('style', 'width: 150px;');
|
||
$form->addInput($watermarkFontColor);
|
||
|
||
// 性能设置部分
|
||
$batchSize = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'batchSize',
|
||
NULL,
|
||
'10',
|
||
'批量处理数量',
|
||
'每次同时处理的图片数量,减少一次性处理压力'
|
||
);
|
||
$batchSize->input->setAttribute('style', 'width: 120px;');
|
||
$form->addInput($batchSize->addRule('isInteger', '请输入整数'));
|
||
|
||
$processDelay = new Typecho_Widget_Helper_Form_Element_Text(
|
||
'processDelay',
|
||
NULL,
|
||
'100',
|
||
'处理延迟(ms)',
|
||
'图片加载后延迟处理的时间(毫秒)'
|
||
);
|
||
$processDelay->input->setAttribute('style', 'width: 120px;');
|
||
$form->addInput($processDelay->addRule('isInteger', '请输入整数'));
|
||
|
||
$debugMode = new Typecho_Widget_Helper_Form_Element_Radio(
|
||
'debugMode',
|
||
array('1' => '开启', '0' => '关闭'),
|
||
'0',
|
||
'调试模式',
|
||
'开启后在控制台显示处理信息(调试完成后建议关闭)'
|
||
);
|
||
$form->addInput($debugMode);
|
||
}
|
||
|
||
/**
|
||
* 个人用户配置面板
|
||
*/
|
||
public static function personalConfig(Typecho_Widget_Helper_Form $form)
|
||
{
|
||
// 不需要个人配置
|
||
}
|
||
|
||
/**
|
||
* 处理文章内容
|
||
*/
|
||
public static function parseContent($content, $widget, $lastResult)
|
||
{
|
||
$content = empty($lastResult) ? $content : $lastResult;
|
||
|
||
if ($widget instanceof Widget_Archive) {
|
||
$content = self::parseImages($content, $widget);
|
||
}
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 解析图片并添加标题和水印
|
||
*/
|
||
private static function parseImages($text, $widget)
|
||
{
|
||
$options = Helper::options();
|
||
$config = $options->plugin('ImageCaptionWatermark');
|
||
|
||
if (!$config) {
|
||
return $text;
|
||
}
|
||
|
||
// 获取当前文章分类
|
||
$currentCategories = array();
|
||
if ($widget->categories && is_array($widget->categories)) {
|
||
foreach ($widget->categories as $category) {
|
||
if (isset($category['name'])) {
|
||
$currentCategories[] = $category['name'];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理显示分类设置
|
||
$titleIncludeCategories = array();
|
||
if (!empty($config->titleIncludeCategories) && trim($config->titleIncludeCategories) !== '') {
|
||
$titleIncludeCategories = array_map('trim', explode(',', $config->titleIncludeCategories));
|
||
}
|
||
|
||
$watermarkIncludeCategories = array();
|
||
if (!empty($config->watermarkIncludeCategories) && trim($config->watermarkIncludeCategories) !== '') {
|
||
$watermarkIncludeCategories = array_map('trim', explode(',', $config->watermarkIncludeCategories));
|
||
}
|
||
|
||
// 检查当前分类是否在显示列表中
|
||
$isTitleIncluded = true; // 默认显示,除非设置了显示分类且当前分类不在其中
|
||
$isWatermarkIncluded = true; // 默认显示,除非设置了显示分类且当前分类不在其中
|
||
|
||
// 如果设置了显示分类,则检查当前分类是否在其中
|
||
if (!empty($titleIncludeCategories)) {
|
||
$isTitleIncluded = false;
|
||
foreach ($currentCategories as $category) {
|
||
if (in_array($category, $titleIncludeCategories)) {
|
||
$isTitleIncluded = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($watermarkIncludeCategories)) {
|
||
$isWatermarkIncluded = false;
|
||
foreach ($currentCategories as $category) {
|
||
if (in_array($category, $watermarkIncludeCategories)) {
|
||
$isWatermarkIncluded = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 匹配图片标签
|
||
$text = preg_replace_callback('/<img\s[^>]*>/i', function($matches) use ($config, $isTitleIncluded, $isWatermarkIncluded) {
|
||
$imgTag = $matches[0];
|
||
|
||
// 跳过特定图片
|
||
if (preg_match('/class=["\'][^"\']*(no-watermark|no-caption)[^"\']*["\']/i', $imgTag)) {
|
||
return $imgTag;
|
||
}
|
||
|
||
// 检查图片是否在横排组中(通过父元素判断)
|
||
$skipTitle = false;
|
||
if ($config->skipFlexGroupTitles == '1') {
|
||
// 检查img标签周围是否有flexphoto-group的迹象
|
||
if (strpos($imgTag, 'no-title') !== false ||
|
||
preg_match('/class=["\'][^"\']*no-title[^"\']*["\']/i', $imgTag)) {
|
||
$skipTitle = true;
|
||
}
|
||
}
|
||
|
||
$newTag = $imgTag;
|
||
|
||
// 获取图片URL
|
||
$imgUrl = '';
|
||
if (preg_match('/src=["\']([^"\']+)["\']/', $imgTag, $srcMatch)) {
|
||
$imgUrl = $srcMatch[1];
|
||
}
|
||
|
||
// 获取标题(从alt或title属性)
|
||
$caption = '';
|
||
$hasChinese = false;
|
||
|
||
if (!$skipTitle) { // 只有需要显示标题时才处理
|
||
if (preg_match('/alt=["\']([^"\']*)["\']/', $imgTag, $altMatch)) {
|
||
$caption = $altMatch[1];
|
||
$hasChinese = self::hasChineseText($caption);
|
||
} elseif (preg_match('/title=["\']([^"\']*)["\']/', $imgTag, $titleMatch)) {
|
||
$caption = $titleMatch[1];
|
||
$hasChinese = self::hasChineseText($caption);
|
||
}
|
||
}
|
||
|
||
// 如果开启了仅显示中文标题,且没有中文,则清空标题
|
||
if ($config->titleChineseOnly == '1' && !$hasChinese) {
|
||
$caption = '';
|
||
} else {
|
||
// 提取中文部分
|
||
$caption = self::extractChineseText($caption);
|
||
}
|
||
|
||
// 添加水印数据属性(如果当前分类在显示列表中)
|
||
$watermarkData = '';
|
||
if ($isWatermarkIncluded && $config->watermarkEnable == '1' && !empty($config->watermarkText)) {
|
||
$watermarkClass = 'icw-watermark';
|
||
$watermarkData = ' data-watermark="' . htmlspecialchars($config->watermarkText) . '"';
|
||
$watermarkData .= ' data-watermark-position="' . htmlspecialchars($config->watermarkPosition) . '"';
|
||
$watermarkData .= ' data-watermark-minwidth="' . intval($config->watermarkMinWidth) . '"';
|
||
$watermarkData .= ' data-watermark-minheight="' . intval($config->watermarkMinHeight) . '"';
|
||
|
||
$newTag = preg_replace('/<img\s/', '<img class="' . $watermarkClass . '"' . $watermarkData, $newTag, 1);
|
||
}
|
||
|
||
// 为所有需要处理的图片添加包装器
|
||
if ((!$skipTitle && $isTitleIncluded && $config->titleEnable == '1' && !empty($caption) && !empty($imgUrl)) ||
|
||
($isWatermarkIncluded && $config->watermarkEnable == '1' && !empty($imgUrl))) {
|
||
|
||
$wrapperClass = 'icw-wrapper';
|
||
|
||
// 添加标题数据(如果当前分类在显示列表中且需要显示标题)
|
||
$titleData = '';
|
||
if (!$skipTitle && $isTitleIncluded && $config->titleEnable == '1' && !empty($caption)) {
|
||
$titleData .= ' data-caption="' . htmlspecialchars($caption) . '"';
|
||
$titleData .= ' data-position="' . htmlspecialchars($config->titlePosition) . '"';
|
||
$titleData .= ' data-minwidth="' . intval($config->titleMinWidth) . '"';
|
||
$titleData .= ' data-minheight="' . intval($config->titleMinHeight) . '"';
|
||
$titleData .= ' data-chinese-only="' . $config->titleChineseOnly . '"';
|
||
|
||
if ($isWatermarkIncluded && $config->watermarkEnable == '1') {
|
||
$wrapperClass .= ' icw-has-both';
|
||
} else {
|
||
$wrapperClass .= ' icw-title-only';
|
||
}
|
||
} elseif ($isWatermarkIncluded && $config->watermarkEnable == '1') {
|
||
$wrapperClass .= ' icw-watermark-only';
|
||
}
|
||
|
||
// 如果是横排组内的图片,添加特殊标记
|
||
if ($skipTitle) {
|
||
$wrapperClass .= ' icw-in-flexgroup';
|
||
}
|
||
|
||
$newTag = '<div class="' . $wrapperClass . '"' . $titleData . '>
|
||
' . $newTag . '
|
||
</div>';
|
||
}
|
||
|
||
return $newTag;
|
||
}, $text);
|
||
|
||
return $text;
|
||
}
|
||
|
||
/**
|
||
* 检查是否包含中文字符
|
||
*/
|
||
private static function hasChineseText($text)
|
||
{
|
||
return preg_match('/[\x{4e00}-\x{9fa5}]/u', $text);
|
||
}
|
||
|
||
/**
|
||
* 提取中文字符
|
||
*/
|
||
private static function extractChineseText($text)
|
||
{
|
||
preg_match_all('/[\x{4e00}-\x{9fa5}\x{3000}-\x{303F}\x{FF00}-\x{FFEF}]+/u', $text, $matches);
|
||
if (!empty($matches[0])) {
|
||
return trim(implode('', $matches[0]));
|
||
}
|
||
return $text;
|
||
}
|
||
|
||
/**
|
||
* 添加页脚脚本和样式(前台)
|
||
*/
|
||
public static function addFooter()
|
||
{
|
||
$options = Helper::options();
|
||
$config = $options->plugin('ImageCaptionWatermark');
|
||
|
||
if (!$config) {
|
||
return;
|
||
}
|
||
|
||
$debugMode = $config->debugMode == '1' ? 'true' : 'false';
|
||
$batchSize = intval($config->batchSize) ?: 10;
|
||
$processDelay = intval($config->processDelay) ?: 100;
|
||
$skipFlexGroupTitles = $config->skipFlexGroupTitles == '1' ? 'true' : 'false';
|
||
|
||
// 输出CSS样式
|
||
echo '<style id="icw-styles">
|
||
/* ============================================ */
|
||
/* 图片标题和水印插件 - 双功能同步版 v3.5 */
|
||
/* ============================================ */
|
||
|
||
/* 1. 图片容器 - 确保不干扰现有布局 */
|
||
.icw-wrapper {
|
||
position: relative !important;
|
||
display: inline-block !important;
|
||
max-width: 100% !important;
|
||
margin: 15px 0 !important;
|
||
line-height: 0 !important;
|
||
overflow: visible !important;
|
||
vertical-align: middle !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
|
||
/* 保持与flexphoto兼容 */
|
||
.flexphoto .icw-wrapper {
|
||
margin: 0 !important;
|
||
flex: 1 !important;
|
||
}
|
||
|
||
/* 横排组内的图片容器特殊处理 */
|
||
.icw-wrapper.icw-in-flexgroup {
|
||
margin: 0 !important;
|
||
}
|
||
|
||
/* 2. 图片样式 */
|
||
.icw-wrapper img {
|
||
display: block !important;
|
||
max-width: 100% !important;
|
||
height: auto !important;
|
||
width: 100% !important;
|
||
vertical-align: middle !important;
|
||
border-radius: 10px !important;
|
||
}
|
||
|
||
/* flexphoto中的图片特殊处理 */
|
||
.flexphoto .icw-wrapper img {
|
||
height: 100% !important;
|
||
width: 100% !important;
|
||
object-fit: cover !important;
|
||
border-radius: 0 !important;
|
||
}
|
||
|
||
/* 3. 标题样式 */
|
||
.icw-caption {
|
||
position: absolute !important;
|
||
z-index: 100 !important;
|
||
box-sizing: border-box !important;
|
||
max-width: 100% !important;
|
||
overflow: hidden !important;
|
||
text-overflow: ellipsis !important;
|
||
white-space: nowrap !important;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
||
font-weight: 500 !important;
|
||
line-height: 1.4 !important;
|
||
pointer-events: none !important;
|
||
opacity: 1 !important;
|
||
visibility: visible !important;
|
||
display: block !important;
|
||
color: ' . $config->titleColor . ' !important;
|
||
background-color: ' . $config->titleBgColor . ' !important;
|
||
font-size: ' . $config->titleFontSize . 'px !important;
|
||
padding: ' . $config->titlePadding . ' !important;
|
||
border-radius: 0 !important;
|
||
box-shadow: none !important;
|
||
opacity: 1 !important;
|
||
}
|
||
|
||
/* 标题背景透明度 - 新增功能 */
|
||
.icw-caption {
|
||
background-color: ' . str_replace(')', ', ' . floatval($config->titleBgOpacity) . ')',
|
||
str_replace('rgba(', 'rgba(', $config->titleBgColor)) . ' !important;
|
||
}
|
||
|
||
/* 如果背景色不是rgba格式,转换为rgba格式并添加透明度 */
|
||
.icw-caption:not([style*="rgba"]) {
|
||
background-color: rgba(' . (hexdec(substr($config->titleBgColor, 1, 2)) ?: 0) . ', ' .
|
||
(hexdec(substr($config->titleBgColor, 3, 2)) ?: 0) . ', ' .
|
||
(hexdec(substr($config->titleBgColor, 5, 2)) ?: 0) . ', ' .
|
||
floatval($config->titleBgOpacity) . ') !important;
|
||
}
|
||
|
||
/* 标题位置 */
|
||
.caption-bottom-center {
|
||
bottom: 0 !important;
|
||
left: 0 !important;
|
||
right: 0 !important;
|
||
width: 100% !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
.caption-bottom-right {
|
||
bottom: 0 !important;
|
||
right: 0 !important;
|
||
}
|
||
|
||
.caption-bottom-left {
|
||
bottom: 0 !important;
|
||
left: 0 !important;
|
||
}
|
||
|
||
.caption-top-center {
|
||
top: 0 !important;
|
||
left: 0 !important;
|
||
right: 0 !important;
|
||
width: 100% !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
.caption-top-right {
|
||
top: 0 !important;
|
||
right: 0 !important;
|
||
}
|
||
|
||
.caption-top-left {
|
||
top: 0 !important;
|
||
left: 0 !important;
|
||
}
|
||
|
||
/* 4. 水印样式 */
|
||
.icw-watermark-element {
|
||
position: absolute !important;
|
||
z-index: 99 !important;
|
||
color: ' . $config->watermarkFontColor . ' !important;
|
||
font-size: ' . $config->watermarkFontSize . 'px !important;
|
||
font-weight: bold !important;
|
||
opacity: ' . $config->watermarkOpacity . ' !important;
|
||
text-shadow: 1px 1px 3px rgba(0,0,0,0.5) !important;
|
||
pointer-events: none !important;
|
||
font-family: Arial, sans-serif !important;
|
||
white-space: nowrap !important;
|
||
}
|
||
|
||
/* 水印位置 */
|
||
.icw-watermark-element.watermark-top-left {
|
||
top: 25px !important;
|
||
left: 25px !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-top-center {
|
||
top: 25px !important;
|
||
left: 50% !important;
|
||
transform: translateX(-50%) !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-top-right {
|
||
top: 25px !important;
|
||
right: 25px !important;
|
||
text-align: right !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-center {
|
||
top: 50% !important;
|
||
left: 50% !important;
|
||
transform: translate(-50%, -50%) !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-bottom-left {
|
||
bottom: 45px !important;
|
||
left: 25px !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-bottom-center {
|
||
bottom: 45px !important;
|
||
left: 50% !important;
|
||
transform: translateX(-50%) !important;
|
||
text-align: center !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-bottom-right {
|
||
bottom: 45px !important;
|
||
right: 25px !important;
|
||
text-align: right !important;
|
||
}
|
||
|
||
/* 5. 横排组特殊处理 */
|
||
.flexphoto-group .icw-caption {
|
||
display: none !important;
|
||
}
|
||
|
||
/* 6. 夜间模式适配 */
|
||
body.dark .icw-caption {
|
||
color: #faf8f1 !important;
|
||
background-color: rgba(10, 12, 25, 0.9) !important;
|
||
}
|
||
|
||
body.dark .icw-watermark-element {
|
||
color: rgba(255, 255, 255, 0.8) !important;
|
||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7) !important;
|
||
}
|
||
|
||
/* 7. 移动端适配 */
|
||
@media screen and (max-width: 767px) {
|
||
.icw-caption {
|
||
font-size: ' . (intval($config->titleFontSize) - 2) . 'px !important;
|
||
padding: 6px 10px !important;
|
||
}
|
||
|
||
.icw-watermark-element {
|
||
font-size: ' . (intval($config->watermarkFontSize) - 6) . 'px !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-top-left,
|
||
.icw-watermark-element.watermark-bottom-left {
|
||
left: 15px !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-top-right,
|
||
.icw-watermark-element.watermark-bottom-right {
|
||
right: 15px !important;
|
||
}
|
||
|
||
.icw-watermark-element.watermark-bottom-left,
|
||
.icw-watermark-element.watermark-bottom-center,
|
||
.icw-watermark-element.watermark-bottom-right {
|
||
bottom: 35px !important;
|
||
}
|
||
}
|
||
|
||
/* 8. 覆盖主题可能冲突的样式 */
|
||
.markdown-body .icw-wrapper img {
|
||
max-width: 100% !important;
|
||
margin: 0 auto !important;
|
||
box-sizing: content-box !important;
|
||
background-color: transparent !important;
|
||
}
|
||
|
||
body.dark .markdown-body .icw-wrapper img {
|
||
background-color: inherit !important;
|
||
}
|
||
</style>';
|
||
|
||
// 输出JavaScript - 修改以支持跳过横排组标题
|
||
echo '<script id="icw-script">
|
||
(function() {
|
||
"use strict";
|
||
|
||
var ICW = {
|
||
// 配置参数
|
||
config: {
|
||
debug: ' . $debugMode . ',
|
||
titleEnable: ' . ($config->titleEnable == '1' ? 'true' : 'false') . ',
|
||
watermarkEnable: ' . ($config->watermarkEnable == '1' ? 'true' : 'false') . ',
|
||
skipFlexGroupTitles: ' . $skipFlexGroupTitles . ',
|
||
titleMinWidth: ' . intval($config->titleMinWidth) . ',
|
||
titleMinHeight: ' . intval($config->titleMinHeight) . ',
|
||
watermarkMinWidth: ' . intval($config->watermarkMinWidth) . ',
|
||
watermarkMinHeight: ' . intval($config->watermarkMinHeight) . ',
|
||
titleChineseOnly: ' . ($config->titleChineseOnly == '1' ? 'true' : 'false') . ',
|
||
watermarkPosition: "' . htmlspecialchars($config->watermarkPosition) . '",
|
||
watermarkFontSize: "' . intval($config->watermarkFontSize) . '",
|
||
watermarkFontColor: "' . htmlspecialchars($config->watermarkFontColor) . '",
|
||
watermarkOpacity: "' . floatval($config->watermarkOpacity) . '",
|
||
batchSize: ' . $batchSize . ',
|
||
processDelay: ' . $processDelay . '
|
||
},
|
||
|
||
// 状态管理
|
||
state: {
|
||
processedWrappers: new Set(),
|
||
isProcessing: false,
|
||
processingQueue: []
|
||
},
|
||
|
||
// 日志函数
|
||
log: function() {
|
||
if (this.config.debug) {
|
||
console.log("[ICW]", ...arguments);
|
||
}
|
||
},
|
||
|
||
// 检查是否包含中文
|
||
hasChineseText: function(text) {
|
||
if (!text) return false;
|
||
var chineseRegex = /[\\u4e00-\\u9fa5]/;
|
||
return chineseRegex.test(text);
|
||
},
|
||
|
||
// 获取包装器唯一标识
|
||
getWrapperId: function(wrapper) {
|
||
var img = wrapper.querySelector("img");
|
||
if (img && img.src) {
|
||
return img.src;
|
||
}
|
||
return wrapper.outerHTML;
|
||
},
|
||
|
||
// 检查是否在横排组中
|
||
isInFlexGroup: function(wrapper) {
|
||
// 检查是否在flexphoto-group中
|
||
var parentFlexGroup = wrapper.closest(".flexphoto-group");
|
||
if (parentFlexGroup) {
|
||
return true;
|
||
}
|
||
|
||
// 检查是否有特定类名
|
||
if (wrapper.classList.contains("icw-in-flexgroup")) {
|
||
return true;
|
||
}
|
||
|
||
// 检查图片是否有no-title类
|
||
var img = wrapper.querySelector("img");
|
||
if (img && (img.classList.contains("no-title") ||
|
||
img.getAttribute("class") && img.getAttribute("class").includes("no-title"))) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
},
|
||
|
||
// 初始化插件
|
||
init: function() {
|
||
this.log("插件初始化开始");
|
||
this.log("标题功能:", this.config.titleEnable ? "开启" : "关闭");
|
||
this.log("水印功能:", this.config.watermarkEnable ? "开启" : "关闭");
|
||
this.log("跳过横排组标题:", this.config.skipFlexGroupTitles ? "是" : "否");
|
||
this.log("标题最小尺寸:", this.config.titleMinWidth + "×" + this.config.titleMinHeight + "px");
|
||
this.log("水印最小尺寸:", this.config.watermarkMinWidth + "×" + this.config.watermarkMinHeight + "px");
|
||
|
||
// 启动统一处理
|
||
setTimeout(this.startUnifiedProcessing.bind(this), 300);
|
||
},
|
||
|
||
// ============ 统一处理逻辑 ============
|
||
startUnifiedProcessing: function() {
|
||
this.log("开始统一处理");
|
||
|
||
// 收集所有需要处理的包装器
|
||
var wrappers = document.querySelectorAll(".icw-wrapper");
|
||
this.log("找到需要处理的包装器:", wrappers.length);
|
||
|
||
if (wrappers.length === 0) {
|
||
this.log("没有需要处理的图片");
|
||
return;
|
||
}
|
||
|
||
// 初始化处理队列
|
||
this.state.processingQueue = Array.from(wrappers);
|
||
|
||
// 开始处理队列
|
||
this.processUnifiedQueue();
|
||
},
|
||
|
||
// 处理统一队列
|
||
processUnifiedQueue: function() {
|
||
if (this.state.isProcessing || this.state.processingQueue.length === 0) {
|
||
return;
|
||
}
|
||
|
||
this.state.isProcessing = true;
|
||
|
||
var batchSize = Math.min(this.config.batchSize, this.state.processingQueue.length);
|
||
var batch = this.state.processingQueue.splice(0, batchSize);
|
||
var processedInBatch = 0;
|
||
|
||
this.log("统一批次处理: 本次处理 " + batch.length + " 个包装器");
|
||
|
||
// 处理批次中的每个包装器
|
||
var processNextWrapper = function(index) {
|
||
if (index >= batch.length) {
|
||
// 批次处理完成
|
||
ICW.state.isProcessing = false;
|
||
|
||
ICW.log("批次处理完成,已处理 " + processedInBatch + " 个包装器");
|
||
|
||
// 如果队列中还有包装器,继续处理
|
||
if (ICW.state.processingQueue.length > 0) {
|
||
setTimeout(function() {
|
||
ICW.processUnifiedQueue();
|
||
}, 200);
|
||
} else {
|
||
ICW.log("所有处理完成!");
|
||
|
||
// 最终检查
|
||
setTimeout(ICW.finalCheck.bind(ICW), 1000);
|
||
}
|
||
return;
|
||
}
|
||
|
||
var wrapper = batch[index];
|
||
var wrapperId = ICW.getWrapperId(wrapper);
|
||
|
||
// 检查是否已处理过
|
||
if (ICW.state.processedWrappers.has(wrapperId)) {
|
||
processNextWrapper(index + 1);
|
||
return;
|
||
}
|
||
|
||
ICW.processSingleWrapper(wrapper).then(function(success) {
|
||
if (success) {
|
||
ICW.state.processedWrappers.add(wrapperId);
|
||
processedInBatch++;
|
||
}
|
||
// 处理下一个包装器
|
||
setTimeout(function() {
|
||
processNextWrapper(index + 1);
|
||
}, 50);
|
||
}).catch(function(error) {
|
||
ICW.log("包装器处理失败:", error, wrapper);
|
||
setTimeout(function() {
|
||
processNextWrapper(index + 1);
|
||
}, 50);
|
||
});
|
||
};
|
||
|
||
// 开始处理批次
|
||
processNextWrapper(0);
|
||
},
|
||
|
||
// 处理单个包装器(标题和水印一起处理)
|
||
processSingleWrapper: function(wrapper) {
|
||
return new Promise(function(resolve) {
|
||
var img = wrapper.querySelector("img");
|
||
if (!img) {
|
||
resolve(false);
|
||
return;
|
||
}
|
||
|
||
ICW.waitForImageReady(img).then(function() {
|
||
var displayWidth = wrapper.offsetWidth || img.offsetWidth || img.width;
|
||
var displayHeight = wrapper.offsetHeight || img.offsetHeight || img.height;
|
||
|
||
// 检查是否在横排组中且需要跳过标题
|
||
var isInFlexGroup = ICW.isInFlexGroup(wrapper);
|
||
var skipTitle = ICW.config.skipFlexGroupTitles && isInFlexGroup;
|
||
|
||
// 处理标题
|
||
var titleSuccess = false;
|
||
if (!skipTitle) {
|
||
titleSuccess = ICW.processTitleForWrapper(wrapper, displayWidth, displayHeight);
|
||
} else {
|
||
ICW.log("跳过横排组内的图片标题:", img.src);
|
||
}
|
||
|
||
// 处理水印
|
||
var watermarkSuccess = ICW.processWatermarkForWrapper(wrapper, displayWidth, displayHeight);
|
||
|
||
resolve(titleSuccess || watermarkSuccess);
|
||
}).catch(function(error) {
|
||
ICW.log("图片加载失败,跳过处理:", img.src, error);
|
||
resolve(false);
|
||
});
|
||
});
|
||
},
|
||
|
||
// 为包装器处理标题
|
||
processTitleForWrapper: function(wrapper, displayWidth, displayHeight) {
|
||
if (!ICW.config.titleEnable) {
|
||
return false;
|
||
}
|
||
|
||
var caption = wrapper.getAttribute("data-caption");
|
||
if (!caption || caption.trim() === "") {
|
||
return false;
|
||
}
|
||
|
||
var position = wrapper.getAttribute("data-position") || "bottom-center";
|
||
var minWidth = parseInt(wrapper.getAttribute("data-minwidth")) || ICW.config.titleMinWidth;
|
||
var minHeight = parseInt(wrapper.getAttribute("data-minheight")) || ICW.config.titleMinHeight;
|
||
var chineseOnly = wrapper.getAttribute("data-chinese-only") === "1";
|
||
|
||
// 检查是否仅显示中文标题
|
||
if (chineseOnly && !ICW.hasChineseText(caption)) {
|
||
return false;
|
||
}
|
||
|
||
// 检查图片是否足够大(需要同时满足宽度和高度限制)
|
||
if ((displayWidth < minWidth && minWidth > 0) ||
|
||
(displayHeight < minHeight && minHeight > 0) ||
|
||
displayWidth === 0 || displayHeight === 0) {
|
||
ICW.log("图片太小,跳过标题:", caption.substring(0, 30) + "...",
|
||
"实际尺寸:", displayWidth + "×" + displayHeight + "px",
|
||
"要求尺寸:", minWidth + "×" + minHeight + "px");
|
||
return false;
|
||
}
|
||
|
||
// 移除旧的标题(如果有)
|
||
var oldCaption = wrapper.querySelector(".icw-caption");
|
||
if (oldCaption) {
|
||
oldCaption.remove();
|
||
}
|
||
|
||
// 创建新标题
|
||
var captionEl = document.createElement("div");
|
||
captionEl.className = "icw-caption caption-" + position;
|
||
captionEl.textContent = caption;
|
||
captionEl.style.zIndex = "100";
|
||
|
||
wrapper.appendChild(captionEl);
|
||
|
||
ICW.log("标题添加成功:", caption.substring(0, 30) + "...",
|
||
"图片尺寸:", displayWidth + "×" + displayHeight + "px");
|
||
return true;
|
||
},
|
||
|
||
// 为包装器处理水印
|
||
processWatermarkForWrapper: function(wrapper, displayWidth, displayHeight) {
|
||
if (!ICW.config.watermarkEnable) {
|
||
return false;
|
||
}
|
||
|
||
var img = wrapper.querySelector("img.icw-watermark");
|
||
if (!img) {
|
||
return false;
|
||
}
|
||
|
||
var watermarkText = img.getAttribute("data-watermark");
|
||
var watermarkPosition = img.getAttribute("data-watermark-position") || ICW.config.watermarkPosition;
|
||
var watermarkMinWidth = parseInt(img.getAttribute("data-watermark-minwidth")) || ICW.config.watermarkMinWidth;
|
||
var watermarkMinHeight = parseInt(img.getAttribute("data-watermark-minheight")) || ICW.config.watermarkMinHeight;
|
||
|
||
if (!watermarkText) {
|
||
return false;
|
||
}
|
||
|
||
// 检查图片是否足够大(需要同时满足宽度和高度限制)
|
||
if ((displayWidth < watermarkMinWidth && watermarkMinWidth > 0) ||
|
||
(displayHeight < watermarkMinHeight && watermarkMinHeight > 0) ||
|
||
displayWidth === 0 || displayHeight === 0) {
|
||
ICW.log("图片太小,跳过水印:", img.src.substring(0, 50) + "...",
|
||
"实际尺寸:", displayWidth + "×" + displayHeight + "px",
|
||
"要求尺寸:", watermarkMinWidth + "×" + watermarkMinHeight + "px");
|
||
return false;
|
||
}
|
||
|
||
// 移除旧的水印(如果有)
|
||
var existingWatermark = wrapper.querySelector(".icw-watermark-element");
|
||
if (existingWatermark) {
|
||
existingWatermark.remove();
|
||
}
|
||
|
||
// 创建水印元素
|
||
var watermarkEl = document.createElement("div");
|
||
watermarkEl.className = "icw-watermark-element watermark-" + watermarkPosition.replace("-", "-");
|
||
watermarkEl.textContent = watermarkText;
|
||
|
||
// 应用水印样式
|
||
watermarkEl.style.color = ICW.config.watermarkFontColor;
|
||
watermarkEl.style.fontSize = ICW.config.watermarkFontSize + "px";
|
||
watermarkEl.style.opacity = ICW.config.watermarkOpacity;
|
||
|
||
wrapper.appendChild(watermarkEl);
|
||
|
||
ICW.log("水印添加成功:", watermarkText, "位置:", watermarkPosition,
|
||
"图片:", img.src.substring(0, 50) + "...",
|
||
"图片尺寸:", displayWidth + "×" + displayHeight + "px");
|
||
return true;
|
||
},
|
||
|
||
// 最终检查
|
||
finalCheck: function() {
|
||
this.log("进行最终检查");
|
||
|
||
// 检查所有包装器
|
||
var allWrappers = document.querySelectorAll(".icw-wrapper");
|
||
var missedWrappers = [];
|
||
|
||
for (var i = 0; i < allWrappers.length; i++) {
|
||
var wrapper = allWrappers[i];
|
||
var wrapperId = this.getWrapperId(wrapper);
|
||
|
||
if (!this.state.processedWrappers.has(wrapperId)) {
|
||
missedWrappers.push(wrapper);
|
||
}
|
||
}
|
||
|
||
if (missedWrappers.length > 0) {
|
||
this.log("发现遗漏的包装器:", missedWrappers.length);
|
||
this.state.processingQueue = missedWrappers;
|
||
this.processUnifiedQueue();
|
||
}
|
||
},
|
||
|
||
// 等待图片准备就绪
|
||
waitForImageReady: function(img) {
|
||
return new Promise(function(resolve, reject) {
|
||
// 如果图片已经加载完成
|
||
if (img.complete && img.naturalWidth > 0) {
|
||
setTimeout(resolve, ICW.config.processDelay);
|
||
return;
|
||
}
|
||
|
||
// 监听加载事件
|
||
var loadHandler = function() {
|
||
img.removeEventListener("load", loadHandler);
|
||
img.removeEventListener("error", errorHandler);
|
||
setTimeout(resolve, ICW.config.processDelay);
|
||
};
|
||
|
||
var errorHandler = function() {
|
||
img.removeEventListener("load", loadHandler);
|
||
img.removeEventListener("error", errorHandler);
|
||
reject(new Error("图片加载失败"));
|
||
};
|
||
|
||
img.addEventListener("load", loadHandler);
|
||
img.addEventListener("error", errorHandler);
|
||
|
||
// 如果图片已经有尺寸,也认为已加载
|
||
if (img.width > 0 || img.height > 0) {
|
||
setTimeout(resolve, ICW.config.processDelay);
|
||
return;
|
||
}
|
||
|
||
// 设置超时(5秒)
|
||
setTimeout(function() {
|
||
img.removeEventListener("load", loadHandler);
|
||
img.removeEventListener("error", errorHandler);
|
||
resolve();
|
||
}, 5000);
|
||
});
|
||
},
|
||
|
||
// 处理窗口大小变化
|
||
handleResize: function() {
|
||
if (this.resizeTimeout) {
|
||
clearTimeout(this.resizeTimeout);
|
||
}
|
||
|
||
this.resizeTimeout = setTimeout(function() {
|
||
ICW.log("窗口大小变化,重新处理");
|
||
|
||
// 移除所有标题和水印
|
||
var titles = document.querySelectorAll(".icw-caption");
|
||
var watermarks = document.querySelectorAll(".icw-watermark-element");
|
||
|
||
titles.forEach(function(title) {
|
||
title.remove();
|
||
});
|
||
|
||
watermarks.forEach(function(watermark) {
|
||
watermark.remove();
|
||
});
|
||
|
||
// 重置状态
|
||
ICW.state.processedWrappers.clear();
|
||
ICW.state.processingQueue = [];
|
||
|
||
// 重新处理
|
||
setTimeout(function() {
|
||
ICW.startUnifiedProcessing();
|
||
}, 300);
|
||
}, 500);
|
||
},
|
||
|
||
// 处理动态加载的内容
|
||
handleDynamicContent: function() {
|
||
this.log("检测到动态内容");
|
||
|
||
setTimeout(function() {
|
||
var newWrappers = document.querySelectorAll(".icw-wrapper:not([data-processed])");
|
||
if (newWrappers.length > 0) {
|
||
ICW.log("发现新的包装器:", newWrappers.length);
|
||
// 添加到队列
|
||
ICW.state.processingQueue.push(...Array.from(newWrappers));
|
||
|
||
// 如果没有正在处理,开始处理
|
||
if (!ICW.state.isProcessing) {
|
||
ICW.processUnifiedQueue();
|
||
}
|
||
}
|
||
}, 500);
|
||
}
|
||
};
|
||
|
||
// 初始化插件
|
||
function initPlugin() {
|
||
setTimeout(function() {
|
||
ICW.init();
|
||
}, 300);
|
||
}
|
||
|
||
// DOM加载完成后初始化
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", initPlugin);
|
||
} else {
|
||
initPlugin();
|
||
}
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener("resize", function() {
|
||
ICW.handleResize();
|
||
});
|
||
|
||
// 监听DOM变化
|
||
var observer = new MutationObserver(function(mutations) {
|
||
var hasNewContent = false;
|
||
|
||
mutations.forEach(function(mutation) {
|
||
if (mutation.addedNodes.length) {
|
||
mutation.addedNodes.forEach(function(node) {
|
||
if (node.nodeType === 1) {
|
||
if (node.tagName === "IMG" || node.tagName === "DIV" ||
|
||
(node.querySelector && node.querySelector("img"))) {
|
||
hasNewContent = true;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
if (hasNewContent) {
|
||
ICW.handleDynamicContent();
|
||
}
|
||
});
|
||
|
||
// 开始观察DOM变化
|
||
setTimeout(function() {
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
}, 1000);
|
||
|
||
// 导出到全局以便调试
|
||
window.ICW = ICW;
|
||
|
||
})();
|
||
</script>';
|
||
}
|
||
} |