PluginProbe
WPWaterMark 轻水印插件 / 5.1.7
WPWaterMark 轻水印插件 v5.1.7
5.2.4 5.2.2 5.2.1 5.1.7 5.1.6 trunk 4.3 5.0.0 5.0.1 5.1.2 5.1.3 5.1.4 5.1.5
wpwatermark / WaterMarkHandler.php

WaterMarkHandler.php in WPWaterMark 轻水印插件 5.1.7, at WaterMarkHandler.php

635 lines 23.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WaterMark Handler Class
4 *
5 * Handles all watermark related operations with improved organization and caching
6 */
7 class WaterMarkHandler {
8 /** @var string */
9 private $cache_dir;
10
11 /** @var string */
12 private $font_dir;
13
14 /** @var array */
15 private $options;
16
17 /**
18 * 支持添加水印的 MIME 类型
19 *
20 * @return string[]
21 */
22 public static function getSupportedMimeTypes(): array {
23 $mimes = ['image/jpeg', 'image/png', 'image/gif'];
24 if (function_exists('imagecreatefromwebp') && function_exists('imagewebp')) {
25 $mimes[] = 'image/webp';
26 }
27 return $mimes;
28 }
29
30 /**
31 * 支持添加水印的扩展名(不带点)
32 *
33 * @return string[]
34 */
35 public static function getSupportedExtensions(): array {
36 $extensions = ['jpg', 'jpeg', 'png', 'gif'];
37 if (function_exists('imagecreatefromwebp') && function_exists('imagewebp')) {
38 $extensions[] = 'webp';
39 }
40 return $extensions;
41 }
42
43 /**
44 * Constructor
45 *
46 * @param array $options Watermark options
47 */
48 public function __construct(array $options) {
49 $this->options = $options;
50 $this->cache_dir = plugin_dir_path(__FILE__) . 'cache/';
51 $this->font_dir = plugin_dir_path(__FILE__) . 'fonts/';
52
53 // Ensure cache directory exists
54 if (!file_exists($this->cache_dir)) {
55 wp_mkdir_p($this->cache_dir);
56 }
57 }
58
59 /**
60 * Generate cache key for watermark options
61 *
62 * @param string $img_url
63 * @param array $options
64 * @return string
65 */
66 private function generateCacheKey(string $img_url, array $options): string {
67 return md5($img_url . serialize($options));
68 }
69
70 /**
71 * 九宫格位置键名列表(与 calculatePosition 的 case 一致)
72 *
73 * @return string[]
74 */
75 private static function getGridPositionKeys(): array {
76 return [
77 'top-left', 'top-center', 'top-right',
78 'middle-left', 'middle-center', 'middle-right',
79 'bottom-left', 'bottom-center', 'bottom-right',
80 ];
81 }
82
83 /**
84 * 若设置为 random,则每次处理在九宫格中随机选一格
85 */
86 private function resolveGridPosition(string $position): string {
87 if ($position === 'random') {
88 $grid = self::getGridPositionKeys();
89 return $grid[wp_rand(0, count($grid) - 1)];
90 }
91 return $position;
92 }
93
94 /**
95 * 从本次调用的 options 与默认�
96 �置得到原始位置字符串(可能为 random)
97 */
98 private function getRawWatermarkPosition(array $options): string {
99 if (isset($options['watermark_position']) && $options['watermark_position'] !== '') {
100 return (string) $options['watermark_position'];
101 }
102 if (isset($options['position']) && $options['position'] !== '') {
103 return (string) $options['position'];
104 }
105 return (string) $this->options['watermark_position'];
106 }
107
108 /**
109 * Check if cached version exists
110 *
111 * @param string $cache_key
112 * @return string|false
113 */
114 private function getCachedImage(string $cache_key) {
115 $cache_extensions = ['jpg', 'png', 'gif', 'webp'];
116 foreach ($cache_extensions as $ext) {
117 $cache_file = $this->cache_dir . $cache_key . '.' . $ext;
118 if (file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) {
119 return $cache_file;
120 }
121 }
122 return false;
123 }
124
125 /**
126 * Create text watermark with improved error handling and caching
127 *
128 * @param string $img_url
129 * @param string $new_img_url
130 * @param string $text
131 * @param array $options
132 * @return bool
133 */
134 public function createTextWatermark(string $img_url, string $new_img_url, string $text, array $options = []): bool {
135 try {
136 $merged = array_merge($this->options, $options);
137 $raw_position = $this->getRawWatermarkPosition($options);
138 $use_cache = ($raw_position !== 'random');
139
140 if ($use_cache) {
141 $cache_key = $this->generateCacheKey($img_url, $merged);
142 $cached_file = $this->getCachedImage($cache_key);
143 if ($cached_file) {
144 copy($cached_file, $new_img_url);
145 return true;
146 }
147 }
148
149 // Validate image
150 $img_size = getimagesize($img_url);
151 if (empty($img_size)) {
152 throw new Exception('Invalid image file');
153 }
154
155 // Validate dimensions
156 if ($img_size[0] < $this->options['watermark_min_width'] ||
157 $img_size[1] < $this->options['watermark_min_height']) {
158 throw new Exception('Image dimensions too small for watermark');
159 }
160
161 // Create image resource
162 $im = $this->createImageResource($img_url, $img_size['mime']);
163 if (!$im) {
164 throw new Exception('Failed to create image resource');
165 }
166
167 // Apply text watermark
168 $text_color = $this->parseColor($options['text_color'] ?? $this->options['text_color']);
169 $font_file = $this->font_dir . ($options['text_font'] ?? $this->options['text_font']);
170
171 if (!file_exists($font_file)) {
172 throw new Exception('Font file not found');
173 }
174
175 $opacity = intval($options['watermark_diaphaneity'] ?? $this->options['watermark_diaphaneity']);
176 $opacity = min(100, max(0, $opacity));
177 $alpha = intval(round((100 - $opacity) * 127 / 100)); // 0(不透明)-127(�
178 �透明)
179 $text_color = imagecolorallocatealpha($im, $text_color['r'], $text_color['g'], $text_color['b'], $alpha);
180 $position = $this->calculatePosition(
181 $this->resolveGridPosition($raw_position),
182 $img_size[0],
183 $img_size[1],
184 $text
185 );
186
187 // Add watermark
188 imagettftext(
189 $im,
190 $options['text_size'] ?? $this->options['text_size'],
191 $options['text_angle'] ?? $this->options['text_angle'],
192 $position['x'],
193 $position['y'],
194 $text_color,
195 $font_file,
196 $text
197 );
198
199 // Save image
200 $this->saveImage($im, $new_img_url, $img_size['mime']);
201
202 // Cache result(随机位置不使用缓存,避�
203 �多次上传被同一随机结果锁死)
204 if ($use_cache) {
205 $cache_ext = 'jpg';
206 if ($img_size['mime'] === 'image/png') {
207 $cache_ext = 'png';
208 } elseif ($img_size['mime'] === 'image/gif') {
209 $cache_ext = 'gif';
210 } elseif ($img_size['mime'] === 'image/webp') {
211 $cache_ext = 'webp';
212 }
213 copy($new_img_url, $this->cache_dir . $cache_key . '.' . $cache_ext);
214 }
215
216 imagedestroy($im);
217 return true;
218
219 } catch (Exception $e) {
220 error_log('WaterMark Error: ' . $e->getMessage());
221 return false;
222 }
223 }
224
225 /**
226 * Create image watermark with improved error handling and caching
227 *
228 * @param string $img_url
229 * @param string $watermark_url
230 * @param string $new_img_url
231 * @param array $options
232 * @return bool
233 */
234 public function createImageWatermark($img_url, $watermark_url, $new_img_url, $options = []) {
235 try {
236 $merged = array_merge($this->options, $options);
237 $raw_position = $this->getRawWatermarkPosition($options);
238 $use_cache = ($raw_position !== 'random');
239
240 if ($use_cache) {
241 $cache_key = $this->generateCacheKey($img_url, $merged);
242 $cached_file = $this->getCachedImage($cache_key);
243 if ($cached_file) {
244 copy($cached_file, $new_img_url);
245 return true;
246 }
247 }
248
249 // Validate image
250 $img_size = getimagesize($img_url);
251 if (empty($img_size)) {
252 throw new Exception('Invalid image file');
253 }
254
255 // Validate dimensions
256 if ($img_size[0] < $this->options['watermark_min_width'] ||
257 $img_size[1] < $this->options['watermark_min_height']) {
258 throw new Exception('Image dimensions too small for watermark');
259 }
260
261 // Create image resource
262 $im = $this->createImageResource($img_url, $img_size['mime']);
263 if (!$im) {
264 throw new Exception('Failed to create image resource');
265 }
266
267 // Load watermark image
268 $watermark_size = getimagesize($watermark_url);
269 if (!$watermark_size) {
270 throw new Exception('Invalid watermark image');
271 }
272
273 $watermark = $this->createImageResource($watermark_url, $watermark_size['mime']);
274 if (!$watermark) {
275 throw new Exception('Failed to create watermark resource');
276 }
277
278 // Calculate position
279 $position = $this->calculatePosition(
280 $this->resolveGridPosition($raw_position),
281 $img_size[0],
282 $img_size[1],
283 '',
284 $watermark_size[0],
285 $watermark_size[1]
286 );
287
288 // 创建临时图像用于处理
289 $temp = imagecreatetruecolor($img_size[0], $img_size[1]);
290
291 // 设置临时图像的透明度支持
292 imagealphablending($temp, false);
293 imagesavealpha($temp, true);
294
295 // 如果原图是PNG,设置透明背景
296 if ($img_size['mime'] === 'image/png' || $img_size['mime'] === 'image/webp') {
297 $transparent = imagecolorallocatealpha($temp, 0, 0, 0, 127);
298 imagefilledrectangle($temp, 0, 0, $img_size[0], $img_size[1], $transparent);
299 }
300
301 // 复制原图到临时图像
302 imagecopy($temp, $im, 0, 0, 0, 0, $img_size[0], $img_size[1]);
303
304 // 获取透明度设置
305 $opacity = ($options['watermark_diaphaneity'] ?? $this->options['watermark_diaphaneity']);
306
307 // 如果是PNG水印,保持�
308 �原有透明度
309 if ($watermark_size['mime'] === 'image/png' || $watermark_size['mime'] === 'image/webp') {
310 // 创建水印临时图像
311 $watermark_temp = imagecreatetruecolor($watermark_size[0], $watermark_size[1]);
312 imagealphablending($watermark_temp, false);
313 imagesavealpha($watermark_temp, true);
314
315 // 设置完�
316 �透明背景
317 $transparent = imagecolorallocatealpha($watermark_temp, 0, 0, 0, 127);
318 imagefilledrectangle($watermark_temp, 0, 0, $watermark_size[0], $watermark_size[1], $transparent);
319
320 // 复制水印到临时图像
321 imagecopy($watermark_temp, $watermark, 0, 0, 0, 0, $watermark_size[0], $watermark_size[1]);
322
323 // 应用用户设置的透明度
324 if ($opacity < 100) {
325 // 逐像素调整透明度
326 for ($x = 0; $x < $watermark_size[0]; $x++) {
327 for ($y = 0; $y < $watermark_size[1]; $y++) {
328 $color = imagecolorsforindex($watermark_temp, imagecolorat($watermark_temp, $x, $y));
329 $alpha = 127 - ((127 - $color['alpha']) * $opacity / 100);
330 $new_color = imagecolorallocatealpha(
331 $watermark_temp,
332 $color['red'],
333 $color['green'],
334 $color['blue'],
335 intval($alpha)
336 );
337 imagesetpixel($watermark_temp, $x, $y, $new_color);
338 }
339 }
340 }
341
342 // 合并水印到目标图像
343 imagealphablending($temp, true);
344 imagecopy($temp, $watermark_temp, $position['x'], $position['y'], 0, 0, $watermark_size[0], $watermark_size[1]);
345 imagedestroy($watermark_temp);
346 } else {
347 // 非PNG水印的处理
348 imagealphablending($temp, true);
349 $this->imagecopymerge_alpha(
350 $temp, $watermark,
351 $position['x'], $position['y'],
352 0, 0,
353 $watermark_size[0], $watermark_size[1],
354 $opacity
355 );
356 }
357
358 // 保存最终图像
359 if ($img_size['mime'] === 'image/png' || $img_size['mime'] === 'image/webp') {
360 imagealphablending($temp, false);
361 imagesavealpha($temp, true);
362 }
363 $this->saveImage($temp, $new_img_url, $img_size['mime']);
364
365 if ($use_cache) {
366 $cache_ext = 'jpg';
367 if ($img_size['mime'] === 'image/png') {
368 $cache_ext = 'png';
369 } elseif ($img_size['mime'] === 'image/gif') {
370 $cache_ext = 'gif';
371 } elseif ($img_size['mime'] === 'image/webp') {
372 $cache_ext = 'webp';
373 }
374 $cache_file = $this->cache_dir . $cache_key . '.' . $cache_ext;
375 copy($new_img_url, $cache_file);
376 }
377
378 // �
379 理资源
380 imagedestroy($temp);
381 imagedestroy($im);
382 imagedestroy($watermark);
383
384 return true;
385
386 } catch (Exception $e) {
387 error_log('WaterMark Error: ' . $e->getMessage());
388 return false;
389 }
390 }
391
392 /**
393 * Helper function to create image resource
394 *
395 * @param string $img_url
396 * @param string $mime_type
397 * @return resource|false
398 */
399 private function createImageResource(string $img_url, string $mime_type) {
400 $create_functions = [
401 'image/jpeg' => 'imagecreatefromjpeg',
402 'image/png' => 'imagecreatefrompng',
403 'image/gif' => 'imagecreatefromgif'
404 ];
405
406 if (function_exists('imagecreatefromwebp')) {
407 $create_functions['image/webp'] = 'imagecreatefromwebp';
408 }
409
410 if (isset($create_functions[$mime_type])) {
411 $im = call_user_func($create_functions[$mime_type], $img_url);
412
413 // 特别处理PNG图片的透明度
414 if ($mime_type === 'image/png' || $mime_type === 'image/webp') {
415 imagealphablending($im, false);
416 imagesavealpha($im, true);
417 }
418
419 return $im;
420 }
421
422 return false;
423 }
424
425 /**
426 * Helper function to save image
427 *
428 * @param resource $im
429 * @param string $filename
430 * @param string $mime_type
431 * @return bool
432 */
433 private function saveImage($im, string $filename, string $mime_type): bool {
434 switch ($mime_type) {
435 case 'image/jpeg':
436 return imagejpeg($im, $filename, 95); // 95% quality for JPEG
437 case 'image/png':
438 return imagepng($im, $filename, 0); // 0-9, 0 for no compression to maintain quality
439 case 'image/gif':
440 return imagegif($im, $filename);
441 case 'image/webp':
442 if (function_exists('imagewebp')) {
443 return imagewebp($im, $filename, 95);
444 }
445 return false;
446 default:
447 return false;
448 }
449 }
450
451 /**
452 * Parse hex color to RGB
453 */
454 private function parseColor($hex_color) {
455 $hex_color = ltrim($hex_color, '#');
456 return [
457 'r' => hexdec(substr($hex_color, 0, 2)),
458 'g' => hexdec(substr($hex_color, 2, 2)),
459 'b' => hexdec(substr($hex_color, 4, 2))
460 ];
461 }
462
463 /**
464 * Helper function for alpha-enabled imagecopymerge
465 *
466 * @param resource $dst_im
467 * @param resource $src_im
468 * @param int $dst_x
469 * @param int $dst_y
470 * @param int $src_x
471 * @param int $src_y
472 * @param int $src_w
473 * @param int $src_h
474 * @param int $pct
475 * @return void
476 */
477 private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) {
478 // 确保透明度在有效范围�
479
480 $pct = min(100, max(0, $pct));
481
482 // 创建临时图像
483 $cut = imagecreatetruecolor($src_w, $src_h);
484
485 // 设置完�
486 �透明背景
487 imagealphablending($cut, false);
488 imagesavealpha($cut, true);
489 $transparent = imagecolorallocatealpha($cut, 0, 0, 0, 127);
490 imagefilledrectangle($cut, 0, 0, $src_w, $src_h, $transparent);
491
492 // 复制目标区域到临时图像
493 imagecopy($cut, $dst_im, 0, 0, intval($dst_x), intval($dst_y), $src_w, $src_h);
494
495 // 启用混合模式
496 imagealphablending($cut, true);
497
498 // 应用水印到临时图像
499 $this->imagecopymerge_alpha_pixel($cut, $src_im, 0, 0, intval($src_x), intval($src_y), $src_w, $src_h, $pct);
500
501 // 保持目标图像的透明度
502 imagealphablending($dst_im, true);
503 imagesavealpha($dst_im, true);
504
505 // 将处理后的临时图像复制回目标图像
506 imagecopy($dst_im, $cut, intval($dst_x), intval($dst_y), 0, 0, $src_w, $src_h);
507
508 // �
509
510 imagedestroy($cut);
511 }
512
513 /**
514 * 逐像素处理透明度
515 */
516 private function imagecopymerge_alpha_pixel($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) {
517 if ($pct == 0) return;
518
519 // 逐像素处理
520 for ($y = 0; $y < $src_h; ++$y) {
521 for ($x = 0; $x < $src_w; ++$x) {
522 $src_color = imagecolorsforindex($src_im, imagecolorat($src_im, $src_x + $x, $src_y + $y));
523 $dst_color = imagecolorsforindex($dst_im, imagecolorat($dst_im, $dst_x + $x, $dst_y + $y));
524
525 // 计算新的透明度
526 $src_alpha = 127 - ($src_color['alpha'] * $pct / 100);
527 $dst_alpha = 127 - $dst_color['alpha'];
528 $final_alpha = 127 - (($src_alpha + $dst_alpha) / 2);
529
530 // 混合颜色
531 $final_red = ($src_color['red'] * $pct + $dst_color['red'] * (100 - $pct)) / 100;
532 $final_green = ($src_color['green'] * $pct + $dst_color['green'] * (100 - $pct)) / 100;
533 $final_blue = ($src_color['blue'] * $pct + $dst_color['blue'] * (100 - $pct)) / 100;
534
535 // 创建新颜色
536 $final_color = imagecolorallocatealpha(
537 $dst_im,
538 intval($final_red),
539 intval($final_green),
540 intval($final_blue),
541 intval($final_alpha)
542 );
543
544 // 设置像素
545 imagesetpixel($dst_im, $dst_x + $x, $dst_y + $y, $final_color);
546 }
547 }
548 }
549
550 /**
551 * Calculate watermark position
552 *
553 * @param string $position
554 * @param int $img_width
555 * @param int $img_height
556 * @param string $text
557 * @param int $mark_width
558 * @param int $mark_height
559 * @return array{x: int, y: int}
560 */
561 private function calculatePosition($position, $img_width, $img_height, $text = '', $mark_width = 0, $mark_height = 0) {
562 $margin = intval($this->options['watermark_margin']);
563
564 // For text watermark
565 if ($text !== '') {
566 $font_size = $this->options['text_size'];
567 $font_file = $this->font_dir . $this->options['text_font'];
568 $text_box = imagettfbbox($font_size, 0, $font_file, $text);
569 $mark_width = abs($text_box[4] - $text_box[0]);
570 $mark_height = abs($text_box[1] - $text_box[5]);
571 }
572
573 // Calculate grid dimensions
574 $grid_width = $img_width / 3;
575 $grid_height = $img_height / 3;
576
577 // Calculate position based on grid
578 switch ($position) {
579 case 'top-left':
580 return [
581 'x' => $margin,
582 'y' => $margin
583 ];
584
585 case 'top-center':
586 return [
587 'x' => intval($grid_width + ($grid_width - $mark_width) / 2),
588 'y' => $margin
589 ];
590
591 case 'top-right':
592 return [
593 'x' => intval($img_width - $mark_width - $margin),
594 'y' => $margin
595 ];
596
597 case 'middle-left':
598 return [
599 'x' => $margin,
600 'y' => intval($grid_height + ($grid_height - $mark_height) / 2)
601 ];
602
603 case 'middle-center':
604 return [
605 'x' => intval($grid_width + ($grid_width - $mark_width) / 2),
606 'y' => intval($grid_height + ($grid_height - $mark_height) / 2)
607 ];
608
609 case 'middle-right':
610 return [
611 'x' => intval($img_width - $mark_width - $margin),
612 'y' => intval($grid_height + ($grid_height - $mark_height) / 2)
613 ];
614
615 case 'bottom-left':
616 return [
617 'x' => $margin,
618 'y' => intval($img_height - $mark_height - $margin)
619 ];
620
621 case 'bottom-center':
622 return [
623 'x' => intval($grid_width + ($grid_width - $mark_width) / 2),
624 'y' => intval($img_height - $mark_height - $margin)
625 ];
626
627 case 'bottom-right':
628 default:
629 return [
630 'x' => intval($img_width - $mark_width - $margin),
631 'y' => intval($img_height - $mark_height - $margin)
632 ];
633 }
634 }
635 }