PluginProbe
WPWaterMark 轻水印插件 / 5.1.5
WPWaterMark 轻水印插件 v5.1.5
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.5, at WaterMarkHandler.php

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