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

777 lines 28.4 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 // 缓存默认�
54 �闭,不再自动创建缓存目录
55 if ($this->isCacheEnabled() && !file_exists($this->cache_dir)) {
56 wp_mkdir_p($this->cache_dir);
57 }
58 }
59
60 /**
61 * Generate cache key for watermark options
62 *
63 * @param string $img_url
64 * @param array $options
65 * @return string
66 */
67 private function generateCacheKey(string $img_url, array $options): string {
68 return md5($img_url . serialize($options));
69 }
70
71 /**
72 * 是否启用水印缓存(默认�
73 �闭,避�
74 �长期占用磁盘)
75 */
76 private function isCacheEnabled(): bool {
77 return false;
78 }
79
80 /**
81 * 九宫格位置键名列表(与 calculatePosition 的 case 一致)
82 *
83 * @return string[]
84 */
85 private static function getGridPositionKeys(): array {
86 return [
87 'top-left', 'top-center', 'top-right',
88 'middle-left', 'middle-center', 'middle-right',
89 'bottom-left', 'bottom-center', 'bottom-right',
90 ];
91 }
92
93 /**
94 * 若设置为 random,则每次处理在九宫格中随机选一格
95 */
96 private function resolveGridPosition(string $position): string {
97 if ($position === 'random') {
98 $grid = self::getGridPositionKeys();
99 return $grid[wp_rand(0, count($grid) - 1)];
100 }
101 return $position;
102 }
103
104 /**
105 * 从本次调用的 options 与默认�
106 �置得到原始位置字符串(可能为 random)
107 */
108 private function getRawWatermarkPosition(array $options): string {
109 if (isset($options['watermark_position']) && $options['watermark_position'] !== '') {
110 return (string) $options['watermark_position'];
111 }
112 if (isset($options['position']) && $options['position'] !== '') {
113 return (string) $options['position'];
114 }
115 return (string) $this->options['watermark_position'];
116 }
117
118 /**
119 * Check if cached version exists
120 *
121 * @param string $cache_key
122 * @return string|false
123 */
124 private function getCachedImage(string $cache_key) {
125 $cache_extensions = ['jpg', 'png', 'gif', 'webp'];
126 foreach ($cache_extensions as $ext) {
127 $cache_file = $this->cache_dir . $cache_key . '.' . $ext;
128 if (file_exists($cache_file) && (time() - filemtime($cache_file) < 3600)) {
129 return $cache_file;
130 }
131 }
132 return false;
133 }
134
135 /**
136 * Create text watermark with improved error handling and caching
137 *
138 * @param string $img_url
139 * @param string $new_img_url
140 * @param string $text
141 * @param array $options
142 * @return bool
143 */
144 public function createTextWatermark(string $img_url, string $new_img_url, string $text, array $options = []): bool {
145 try {
146 $merged = array_merge($this->options, $options);
147 $raw_position = $this->getRawWatermarkPosition($options);
148 $use_cache = ($raw_position !== 'random') && $this->isCacheEnabled();
149
150 if ($use_cache) {
151 $cache_key = $this->generateCacheKey($img_url, $merged);
152 $cached_file = $this->getCachedImage($cache_key);
153 if ($cached_file) {
154 copy($cached_file, $new_img_url);
155 return true;
156 }
157 }
158
159 // Validate image
160 $img_size = getimagesize($img_url);
161 if (empty($img_size)) {
162 throw new Exception('Invalid image file');
163 }
164
165 // Validate dimensions
166 if ($img_size[0] < $this->options['watermark_min_width'] ||
167 $img_size[1] < $this->options['watermark_min_height']) {
168 throw new Exception('Image dimensions too small for watermark');
169 }
170
171 // Create image resource
172 $im = $this->createImageResource($img_url, $img_size['mime']);
173 if (!$im) {
174 throw new Exception('Failed to create image resource');
175 }
176
177 // Apply text watermark
178 $text_color = $this->parseColor($options['text_color'] ?? $this->options['text_color']);
179 $font_file = $this->font_dir . ($options['text_font'] ?? $this->options['text_font']);
180
181 if (!file_exists($font_file)) {
182 throw new Exception('Font file not found');
183 }
184
185 $opacity = intval($options['watermark_diaphaneity'] ?? $this->options['watermark_diaphaneity']);
186 $opacity = min(100, max(0, $opacity));
187 $font_size = intval($options['text_size'] ?? $this->options['text_size']);
188 $font_angle = intval($options['text_angle'] ?? $this->options['text_angle']);
189
190 $position = $this->calculatePosition(
191 $this->resolveGridPosition($raw_position),
192 $img_size[0],
193 $img_size[1],
194 $text,
195 0,
196 0,
197 $font_size,
198 $font_angle,
199 $font_file
200 );
201
202 $this->renderTextLayer(
203 $im,
204 $img_size[0],
205 $img_size[1],
206 $text,
207 $font_file,
208 $font_size,
209 $font_angle,
210 $position['x'],
211 $position['y'],
212 $text_color,
213 $opacity
214 );
215
216 if ($img_size['mime'] === 'image/png' || $img_size['mime'] === 'image/webp') {
217 imagealphablending($im, false);
218 imagesavealpha($im, true);
219 }
220
221 // Save image
222 $this->saveImage($im, $new_img_url, $img_size['mime']);
223
224 // Cache result(随机位置不使用缓存,避�
225 �多次上传被同一随机结果锁死)
226 if ($use_cache) {
227 $cache_ext = 'jpg';
228 if ($img_size['mime'] === 'image/png') {
229 $cache_ext = 'png';
230 } elseif ($img_size['mime'] === 'image/gif') {
231 $cache_ext = 'gif';
232 } elseif ($img_size['mime'] === 'image/webp') {
233 $cache_ext = 'webp';
234 }
235 copy($new_img_url, $this->cache_dir . $cache_key . '.' . $cache_ext);
236 }
237
238 imagedestroy($im);
239 return true;
240
241 } catch (Exception $e) {
242 error_log('WaterMark Error: ' . $e->getMessage());
243 return false;
244 }
245 }
246
247 /**
248 * Create image watermark with improved error handling and caching
249 *
250 * @param string $img_url
251 * @param string $watermark_url
252 * @param string $new_img_url
253 * @param array $options
254 * @return bool
255 */
256 public function createImageWatermark($img_url, $watermark_url, $new_img_url, $options = []) {
257 try {
258 $merged = array_merge($this->options, $options);
259 $raw_position = $this->getRawWatermarkPosition($options);
260 $use_cache = ($raw_position !== 'random') && $this->isCacheEnabled();
261
262 if ($use_cache) {
263 $cache_key = $this->generateCacheKey($img_url, $merged);
264 $cached_file = $this->getCachedImage($cache_key);
265 if ($cached_file) {
266 copy($cached_file, $new_img_url);
267 return true;
268 }
269 }
270
271 // Validate image
272 $img_size = getimagesize($img_url);
273 if (empty($img_size)) {
274 throw new Exception('Invalid image file');
275 }
276
277 // Validate dimensions
278 if ($img_size[0] < $this->options['watermark_min_width'] ||
279 $img_size[1] < $this->options['watermark_min_height']) {
280 throw new Exception('Image dimensions too small for watermark');
281 }
282
283 // Create image resource
284 $im = $this->createImageResource($img_url, $img_size['mime']);
285 if (!$im) {
286 throw new Exception('Failed to create image resource');
287 }
288
289 // Load watermark image
290 $watermark_size = getimagesize($watermark_url);
291 if (!$watermark_size) {
292 throw new Exception('Invalid watermark image');
293 }
294
295 $watermark = $this->createImageResource($watermark_url, $watermark_size['mime']);
296 if (!$watermark) {
297 throw new Exception('Failed to create watermark resource');
298 }
299
300 $scale_percent = intval($options['image_watermark_scale'] ?? $this->options['image_watermark_scale'] ?? 100);
301 $scale_percent = min(100, max(1, $scale_percent));
302 if ($scale_percent < 100) {
303 $scaled_width = max(1, intval(round($watermark_size[0] * $scale_percent / 100)));
304 $scaled_height = max(1, intval(round($watermark_size[1] * $scale_percent / 100)));
305 $scaled_watermark = $this->resizeWatermarkResource(
306 $watermark,
307 $watermark_size[0],
308 $watermark_size[1],
309 $scaled_width,
310 $scaled_height
311 );
312 if ($scaled_watermark) {
313 imagedestroy($watermark);
314 $watermark = $scaled_watermark;
315 $watermark_size[0] = $scaled_width;
316 $watermark_size[1] = $scaled_height;
317 }
318 }
319
320 // Calculate position
321 $position = $this->calculatePosition(
322 $this->resolveGridPosition($raw_position),
323 $img_size[0],
324 $img_size[1],
325 '',
326 $watermark_size[0],
327 $watermark_size[1]
328 );
329
330 // 创建临时图像用于处理
331 $temp = imagecreatetruecolor($img_size[0], $img_size[1]);
332
333 // 设置临时图像的透明度支持
334 imagealphablending($temp, false);
335 imagesavealpha($temp, true);
336
337 // 如果原图是PNG,设置透明背景
338 if ($img_size['mime'] === 'image/png' || $img_size['mime'] === 'image/webp') {
339 $transparent = imagecolorallocatealpha($temp, 0, 0, 0, 127);
340 imagefilledrectangle($temp, 0, 0, $img_size[0], $img_size[1], $transparent);
341 }
342
343 // 复制原图到临时图像
344 imagecopy($temp, $im, 0, 0, 0, 0, $img_size[0], $img_size[1]);
345
346 // 获取透明度设置
347 $opacity = ($options['watermark_diaphaneity'] ?? $this->options['watermark_diaphaneity']);
348
349 // 如果是PNG水印,保持�
350 �原有透明度
351 if ($watermark_size['mime'] === 'image/png' || $watermark_size['mime'] === 'image/webp') {
352 // 创建水印临时图像
353 $watermark_temp = imagecreatetruecolor($watermark_size[0], $watermark_size[1]);
354 imagealphablending($watermark_temp, false);
355 imagesavealpha($watermark_temp, true);
356
357 // 设置完�
358 �透明背景
359 $transparent = imagecolorallocatealpha($watermark_temp, 0, 0, 0, 127);
360 imagefilledrectangle($watermark_temp, 0, 0, $watermark_size[0], $watermark_size[1], $transparent);
361
362 // 复制水印到临时图像
363 imagecopy($watermark_temp, $watermark, 0, 0, 0, 0, $watermark_size[0], $watermark_size[1]);
364
365 // 应用用户设置的透明度
366 if ($opacity < 100) {
367 // 逐像素调整透明度
368 for ($x = 0; $x < $watermark_size[0]; $x++) {
369 for ($y = 0; $y < $watermark_size[1]; $y++) {
370 $color = imagecolorsforindex($watermark_temp, imagecolorat($watermark_temp, $x, $y));
371 $alpha = 127 - ((127 - $color['alpha']) * $opacity / 100);
372 $new_color = imagecolorallocatealpha(
373 $watermark_temp,
374 $color['red'],
375 $color['green'],
376 $color['blue'],
377 intval($alpha)
378 );
379 imagesetpixel($watermark_temp, $x, $y, $new_color);
380 }
381 }
382 }
383
384 // 合并水印到目标图像
385 imagealphablending($temp, true);
386 imagecopy($temp, $watermark_temp, $position['x'], $position['y'], 0, 0, $watermark_size[0], $watermark_size[1]);
387 imagedestroy($watermark_temp);
388 } else {
389 // 非PNG水印的处理
390 imagealphablending($temp, true);
391 $this->imagecopymerge_alpha(
392 $temp, $watermark,
393 $position['x'], $position['y'],
394 0, 0,
395 $watermark_size[0], $watermark_size[1],
396 $opacity
397 );
398 }
399
400 // 保存最终图像
401 if ($img_size['mime'] === 'image/png' || $img_size['mime'] === 'image/webp') {
402 imagealphablending($temp, false);
403 imagesavealpha($temp, true);
404 }
405 $this->saveImage($temp, $new_img_url, $img_size['mime']);
406
407 if ($use_cache) {
408 $cache_ext = 'jpg';
409 if ($img_size['mime'] === 'image/png') {
410 $cache_ext = 'png';
411 } elseif ($img_size['mime'] === 'image/gif') {
412 $cache_ext = 'gif';
413 } elseif ($img_size['mime'] === 'image/webp') {
414 $cache_ext = 'webp';
415 }
416 $cache_file = $this->cache_dir . $cache_key . '.' . $cache_ext;
417 copy($new_img_url, $cache_file);
418 }
419
420 // �
421 理资源
422 imagedestroy($temp);
423 imagedestroy($im);
424 imagedestroy($watermark);
425
426 return true;
427
428 } catch (Exception $e) {
429 error_log('WaterMark Error: ' . $e->getMessage());
430 return false;
431 }
432 }
433
434 /**
435 * Helper function to create image resource
436 *
437 * @param string $img_url
438 * @param string $mime_type
439 * @return resource|false
440 */
441 private function createImageResource(string $img_url, string $mime_type) {
442 $create_functions = [
443 'image/jpeg' => 'imagecreatefromjpeg',
444 'image/png' => 'imagecreatefrompng',
445 'image/gif' => 'imagecreatefromgif'
446 ];
447
448 if (function_exists('imagecreatefromwebp')) {
449 $create_functions['image/webp'] = 'imagecreatefromwebp';
450 }
451
452 if (isset($create_functions[$mime_type])) {
453 $im = call_user_func($create_functions[$mime_type], $img_url);
454
455 // 特别处理PNG图片的透明度
456 if ($mime_type === 'image/png' || $mime_type === 'image/webp') {
457 imagealphablending($im, false);
458 imagesavealpha($im, true);
459 }
460
461 return $im;
462 }
463
464 return false;
465 }
466
467 /**
468 * JPEG/WebP 有损输出质量(与站点 `jpeg_quality` 过滤器对齐后再限制范围)
469 */
470 private function getOutputJpegWebpQuality(): int {
471 $q = isset($this->options['output_jpeg_webp_quality'])
472 ? (int) $this->options['output_jpeg_webp_quality']
473 : 82;
474 $q = max(40, min(100, $q));
475 $filtered = (int) apply_filters('jpeg_quality', $q);
476 return max(40, min(100, $filtered));
477 }
478
479 /**
480 * PNG zlib 压缩级别 0–9(无损,�
481 影响体积与编码耗时)
482 */
483 private function getPngCompressionLevel(): int {
484 $level = isset($this->options['output_png_compression'])
485 ? (int) $this->options['output_png_compression']
486 : 6;
487 $level = max(0, min(9, $level));
488 return max(0, min(9, (int) apply_filters('wpwatermark_png_compression', $level)));
489 }
490
491 /**
492 * Helper function to save image
493 *
494 * @param resource $im
495 * @param string $filename
496 * @param string $mime_type
497 * @return bool
498 */
499 private function saveImage($im, string $filename, string $mime_type): bool {
500 switch ($mime_type) {
501 case 'image/jpeg':
502 return imagejpeg($im, $filename, $this->getOutputJpegWebpQuality());
503 case 'image/png':
504 // 第三参数为压缩级别:0 无压缩体积极大;PNG 无损,提高级别不损画质
505 return imagepng($im, $filename, $this->getPngCompressionLevel());
506 case 'image/gif':
507 return imagegif($im, $filename);
508 case 'image/webp':
509 if (function_exists('imagewebp')) {
510 return imagewebp($im, $filename, $this->getOutputJpegWebpQuality());
511 }
512 return false;
513 default:
514 return false;
515 }
516 }
517
518 /**
519 * Parse hex color to RGB
520 */
521 private function parseColor($hex_color) {
522 $hex_color = ltrim($hex_color, '#');
523 return [
524 'r' => hexdec(substr($hex_color, 0, 2)),
525 'g' => hexdec(substr($hex_color, 2, 2)),
526 'b' => hexdec(substr($hex_color, 4, 2))
527 ];
528 }
529
530 /**
531 * Helper function for alpha-enabled imagecopymerge
532 *
533 * @param resource $dst_im
534 * @param resource $src_im
535 * @param int $dst_x
536 * @param int $dst_y
537 * @param int $src_x
538 * @param int $src_y
539 * @param int $src_w
540 * @param int $src_h
541 * @param int $pct
542 * @return void
543 */
544 private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) {
545 // 确保透明度在有效范围�
546
547 $pct = min(100, max(0, $pct));
548
549 // 创建临时图像
550 $cut = imagecreatetruecolor($src_w, $src_h);
551
552 // 设置完�
553 �透明背景
554 imagealphablending($cut, false);
555 imagesavealpha($cut, true);
556 $transparent = imagecolorallocatealpha($cut, 0, 0, 0, 127);
557 imagefilledrectangle($cut, 0, 0, $src_w, $src_h, $transparent);
558
559 // 复制目标区域到临时图像
560 imagecopy($cut, $dst_im, 0, 0, intval($dst_x), intval($dst_y), $src_w, $src_h);
561
562 // 启用混合模式
563 imagealphablending($cut, true);
564
565 // 应用水印到临时图像
566 $this->imagecopymerge_alpha_pixel($cut, $src_im, 0, 0, intval($src_x), intval($src_y), $src_w, $src_h, $pct);
567
568 // 保持目标图像的透明度
569 imagealphablending($dst_im, true);
570 imagesavealpha($dst_im, true);
571
572 // 将处理后的临时图像复制回目标图像
573 imagecopy($dst_im, $cut, intval($dst_x), intval($dst_y), 0, 0, $src_w, $src_h);
574
575 // �
576
577 imagedestroy($cut);
578 }
579
580 /**
581 * 逐像素处理透明度
582 */
583 private function imagecopymerge_alpha_pixel($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) {
584 if ($pct == 0) return;
585
586 // 逐像素处理
587 for ($y = 0; $y < $src_h; ++$y) {
588 for ($x = 0; $x < $src_w; ++$x) {
589 $src_color = imagecolorsforindex($src_im, imagecolorat($src_im, $src_x + $x, $src_y + $y));
590 $dst_color = imagecolorsforindex($dst_im, imagecolorat($dst_im, $dst_x + $x, $dst_y + $y));
591
592 // 按 Porter-Duff over 合成,避�
593 �边缘泛白/发灰
594 $src_opacity = (1 - ($src_color['alpha'] / 127)) * ($pct / 100);
595 $dst_opacity = 1 - ($dst_color['alpha'] / 127);
596 $out_opacity = $src_opacity + $dst_opacity * (1 - $src_opacity);
597
598 if ($out_opacity <= 0) {
599 continue;
600 }
601
602 $final_red = (($src_color['red'] * $src_opacity) + ($dst_color['red'] * $dst_opacity * (1 - $src_opacity))) / $out_opacity;
603 $final_green = (($src_color['green'] * $src_opacity) + ($dst_color['green'] * $dst_opacity * (1 - $src_opacity))) / $out_opacity;
604 $final_blue = (($src_color['blue'] * $src_opacity) + ($dst_color['blue'] * $dst_opacity * (1 - $src_opacity))) / $out_opacity;
605 $final_alpha = 127 - intval(round($out_opacity * 127));
606
607 // 创建新颜色
608 $final_color = imagecolorallocatealpha(
609 $dst_im,
610 intval(round($final_red)),
611 intval(round($final_green)),
612 intval(round($final_blue)),
613 min(127, max(0, intval($final_alpha)))
614 );
615
616 // 设置像素
617 imagesetpixel($dst_im, $dst_x + $x, $dst_y + $y, $final_color);
618 }
619 }
620 }
621
622 /**
623 * Resize watermark image while preserving alpha channel.
624 */
625 private function resizeWatermarkResource($source, $source_width, $source_height, $target_width, $target_height) {
626 if ($target_width <= 0 || $target_height <= 0) {
627 return false;
628 }
629
630 $target = imagecreatetruecolor($target_width, $target_height);
631 imagealphablending($target, false);
632 imagesavealpha($target, true);
633
634 $transparent = imagecolorallocatealpha($target, 0, 0, 0, 127);
635 imagefilledrectangle($target, 0, 0, $target_width, $target_height, $transparent);
636
637 $success = imagecopyresampled(
638 $target,
639 $source,
640 0,
641 0,
642 0,
643 0,
644 $target_width,
645 $target_height,
646 $source_width,
647 $source_height
648 );
649
650 if (!$success) {
651 imagedestroy($target);
652 return false;
653 }
654
655 imagealphablending($target, true);
656 return $target;
657 }
658
659 /**
660 * Calculate watermark position
661 *
662 * @param string $position
663 * @param int $img_width
664 * @param int $img_height
665 * @param string $text
666 * @param int $mark_width
667 * @param int $mark_height
668 * @return array{x: int, y: int}
669 */
670 private function calculatePosition($position, $img_width, $img_height, $text = '', $mark_width = 0, $mark_height = 0, $font_size = null, $font_angle = null, $font_file = null) {
671 $margin = intval($this->options['watermark_margin']);
672
673 // For text watermark
674 if ($text !== '') {
675 $font_size = $font_size === null ? intval($this->options['text_size']) : intval($font_size);
676 $font_angle = $font_angle === null ? intval($this->options['text_angle']) : intval($font_angle);
677 $font_file = $font_file === null ? $this->font_dir . $this->options['text_font'] : $font_file;
678 $text_box = imagettfbbox($font_size, $font_angle, $font_file, $text);
679 $mark_width = abs($text_box[2] - $text_box[0]);
680 $mark_height = abs($text_box[1] - $text_box[7]);
681 }
682
683 // Calculate grid dimensions
684 $grid_width = $img_width / 3;
685 $grid_height = $img_height / 3;
686
687 // Calculate position based on grid
688 switch ($position) {
689 case 'top-left':
690 return [
691 'x' => $margin,
692 'y' => $margin
693 ];
694
695 case 'top-center':
696 return [
697 'x' => intval($grid_width + ($grid_width - $mark_width) / 2),
698 'y' => $margin
699 ];
700
701 case 'top-right':
702 return [
703 'x' => intval($img_width - $mark_width - $margin),
704 'y' => $margin
705 ];
706
707 case 'middle-left':
708 return [
709 'x' => $margin,
710 'y' => intval($grid_height + ($grid_height - $mark_height) / 2)
711 ];
712
713 case 'middle-center':
714 return [
715 'x' => intval($grid_width + ($grid_width - $mark_width) / 2),
716 'y' => intval($grid_height + ($grid_height - $mark_height) / 2)
717 ];
718
719 case 'middle-right':
720 return [
721 'x' => intval($img_width - $mark_width - $margin),
722 'y' => intval($grid_height + ($grid_height - $mark_height) / 2)
723 ];
724
725 case 'bottom-left':
726 return [
727 'x' => $margin,
728 'y' => intval($img_height - $mark_height - $margin)
729 ];
730
731 case 'bottom-center':
732 return [
733 'x' => intval($grid_width + ($grid_width - $mark_width) / 2),
734 'y' => intval($img_height - $mark_height - $margin)
735 ];
736
737 case 'bottom-right':
738 default:
739 return [
740 'x' => intval($img_width - $mark_width - $margin),
741 'y' => intval($img_height - $mark_height - $margin)
742 ];
743 }
744 }
745
746 /**
747 * Render text on a transparent layer first to avoid edge artifacts.
748 */
749 private function renderTextLayer($base_image, $width, $height, $text, $font_file, $font_size, $font_angle, $x, $y, $rgb, $opacity) {
750 $layer = imagecreatetruecolor($width, $height);
751 imagealphablending($layer, false);
752 imagesavealpha($layer, true);
753
754 $transparent = imagecolorallocatealpha($layer, 0, 0, 0, 127);
755 imagefilledrectangle($layer, 0, 0, $width, $height, $transparent);
756
757 imagealphablending($layer, true);
758 $alpha = intval(round((100 - $opacity) * 127 / 100)); // 0(不透明)-127(�
759 �透明)
760 $text_color = imagecolorallocatealpha($layer, $rgb['r'], $rgb['g'], $rgb['b'], $alpha);
761
762 imagettftext(
763 $layer,
764 $font_size,
765 $font_angle,
766 $x,
767 $y,
768 $text_color,
769 $font_file,
770 $text
771 );
772
773 imagealphablending($base_image, true);
774 imagecopy($base_image, $layer, 0, 0, 0, 0, $width, $height);
775 imagedestroy($layer);
776 }
777 }