首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 圖片加浮水印完整實作:文字/圖片浮水印、位置控制、批次處理

     編輯:PHP 相關知識

圖片浮水印是版權保護、品牌曝光的常用手段。本文提供生產級實作:支援文字/圖片浮水印、9 宮格位置、透明度、旋轉、批次處理與效能優化。

核心類別:Watermarker

<?php
class Watermarker {
    private array $config = [
        'allowedTypes' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
        'maxSize' => 10 * 1024 * 1024, // 10MB
        'jpegQuality' => 90,
        'pngCompression' => 6,
    ];

    public function __construct(array $options = []) {
        $this->config = array_merge($this->config, $options);
    }

    // 主入口:對單一圖片加浮水印
    public function watermark(string $sourcePath, string $destPath, array $options = []): bool {
        $opts = array_merge([
            'type' => 'text',           // 'text' | 'image'
            'content' => '© Watermark', // 文字內容或浮水印圖片路徑
            'position' => 'bottom-right', // 9宮格位置
            'opacity' => 50,            // 0-100
            'fontSize' => 20,
            'fontColor' => '#FFFFFF',
            'fontFile' => null,         // TTF 字體路徑
            'rotation' => 0,            // 旋轉角度
            'margin' => 10,             // 邊距
        ], $options);

        // 1. 驗證來源檔案
        if (!file_exists($sourcePath)) throw new Exception('來源檔案不存在');
        $info = getimagesize($sourcePath);
        if (!$info || !in_array($info['mime'], $this->config['allowedTypes'])) {
            throw new Exception('不支援的圖片格式');
        }
        if (filesize($sourcePath) > $this->config['maxSize']) {
            throw new Exception('檔案過大');
        }

        // 2. 建立來源圖片資源
        $source = $this->createImageResource($sourcePath, $info['mime']);
        if (!$source) throw new Exception('無法載入圖片');

        // 3. 建立浮水印層
        $watermark = $this->createWatermarkLayer($source, $opts);
        if (!$watermark) throw new Exception('建立浮水印失敗');

        // 4. 計算位置並合成
        $this->applyWatermark($source, $watermark, $opts);

        // 5. 輸出
        return $this->saveImage($source, $destPath, $info['mime']);
    }

    // 批次處理
    public function batchWatermark(string $srcDir, string $destDir, array $options = []): array {
        $results = [];
        $files = glob($srcDir . '/*.{jpg,jpeg,png,gif,webp}', GLOB_BRACE);
        
        foreach ($files as $file) {
            $dest = $destDir . '/' . basename($file);
            try {
                $success = $this->watermark($file, $dest, $options);
                $results[] = ['file' => $file, 'success' => $success];
            } catch (Exception $e) {
                $results[] = ['file' => $file, 'success' => false, 'error' => $e->getMessage()];
            }
        }
        return $results;
    }

    private function createImageResource(string $path, string $mime): ?resource {
        return match ($mime) {
            'image/jpeg' => imagecreatefromjpeg($path),
            'image/png' => imagecreatefrompng($path),
            'image/gif' => imagecreatefromgif($path),
            'image/webp' => imagecreatefromwebp($path), // PHP 8.1+
            default => null,
        };
    }

    private function createWatermarkLayer(resource $source, array $opts): ?resource {
        $srcW = imagesx($source);
        $srcH = imagesy($source);
        
        // 建立透明畫布
        $layer = imagecreatetruecolor($srcW, $srcH);
        imagealphablending($layer, false);
        imagesavealpha($layer, true);
        $transparent = imagecolorallocatealpha($layer, 0, 0, 0, 127);
        imagefill($layer, 0, 0, $transparent);

        if ($opts['type'] === 'text') {
            $this->drawTextWatermark($layer, $opts);
        } elseif ($opts['type'] === 'image') {
            $this->drawImageWatermark($layer, $opts);
        }

        // 旋轉 (需 PHP 8.0+ imagerotate 支援 alpha)
        if ($opts['rotation'] !== 0) {
            $layer = imagerotate($layer, $opts['rotation'], $transparent);
        }

        return $layer;
    }

    private function drawTextWatermark(resource $layer, array $opts): void {
        $color = $this->hexToRgba($opts['fontColor'], $opts['opacity']);
        $fontFile = $opts['fontFile'] ?? __DIR__ . '/fonts/NotoSansTC-Regular.otf';
        
        if (!file_exists($fontFile)) {
            // 回退內建字體 (不支援中文)
            imagestring($layer, 5, 10, 10, $opts['content'], $color);
            return;
        }

        // 計算文字範圍
        $bbox = imagettfbbox($opts['fontSize'], 0, $fontFile, $opts['content']);
        $textW = $bbox[2] - $bbox[0];
        $textH = $bbox[1] - $bbox[7];

        // 文字繪製座標 (左下基線)
        $x = ($opts['margin'] < 0) ? (imagesx($layer) + $opts['margin'] - $textW) : $opts['margin'];
        $y = ($opts['margin'] < 0) ? (imagesy($layer) + $opts['margin'] + $textH) : ($opts['margin'] + $textH);
        
        imagettftext($layer, $opts['fontSize'], 0, $x, $y, $color, $fontFile, $opts['content']);
    }

    private function drawImageWatermark(resource $layer, array $opts): void {
        $wmPath = $opts['content'];
        if (!file_exists($wmPath)) return;

        $wmInfo = getimagesize($wmPath);
        $wmResource = $this->createImageResource($wmPath, $wmInfo['mime']);
        if (!$wmResource) return;

        // 調整浮水印大小 (最大佔原圖 30%)
        $maxW = imagesx($layer) * 0.3;
        $maxH = imagesy($layer) * 0.3;
        $wmW = imagesx($wmResource);
        $wmH = imagesy($wmResource);
        
        $scale = min($maxW / $wmW, $maxH / $wmH, 1);
        if ($scale < 1) {
            $newW = (int)($wmW * $scale);
            $newH = (int)($wmH * $scale);
            $resized = imagecreatetruecolor($newW, $newH);
            imagealphablending($resized, false);
            imagesavealpha($resized, true);
            imagecopyresampled($resized, $wmResource, 0, 0, 0, 0, $newW, $newH, $wmW, $wmH);
            imagedestroy($wmResource);
            $wmResource = $resized;
            $wmW = $newW;
            $wmH = $newH;
        }

        // 置中繪製 (位置稍後由 applyWatermark 處理)
        imagecopy($layer, $wmResource, 0, 0, 0, 0, $wmW, $wmH);
        imagedestroy($wmResource);
    }

    private function applyWatermark(resource $source, resource $watermark, array $opts): void {
        $srcW = imagesx($source);
        $srcH = imagesy($source);
        $wmW = imagesx($watermark);
        $wmH = imagesy($watermark);

        $positions = [
            'top-left' => [$opts['margin'], $opts['margin']],
            'top-center' => [($srcW - $wmW) / 2, $opts['margin']],
            'top-right' => [$srcW - $wmW - $opts['margin'], $opts['margin']],
            'middle-left' => [$opts['margin'], ($srcH - $wmH) / 2],
            'center' => [($srcW - $wmW) / 2, ($srcH - $wmH) / 2],
            'middle-right' => [$srcW - $wmW - $opts['margin'], ($srcH - $wmH) / 2],
            'bottom-left' => [$opts['margin'], $srcH - $wmH - $opts['margin']],
            'bottom-center' => [($srcW - $wmW) / 2, $srcH - $wmH - $opts['margin']],
            'bottom-right' => [$srcW - $wmW - $opts['margin'], $srcH - $wmH - $opts['margin']],
        ];

        [$x, $y] = $positions[$opts['position']] ?? $positions['bottom-right'];
        $x = max(0, (int)$x);
        $y = max(0, (int)$y);

        // 透明度混合
        imagecopymerge($source, $watermark, $x, $y, 0, 0, $wmW, $wmH, $opts['opacity']);
    }

    private function saveImage(resource $image, string $path, string $mime): bool {
        return match ($mime) {
            'image/jpeg' => imagejpeg($image, $path, $this->config['jpegQuality']),
            'image/png' => imagepng($image, $path, $this->config['pngCompression']),
            'image/gif' => imagegif($image, $path),
            'image/webp' => imagewebp($image, $path, $this->config['jpegQuality']),
            default => false,
        };
    }

    private function hexToRgba(string $hex, int $opacity): int {
        $hex = ltrim($hex, '#');
        $r = hexdec(substr($hex, 0, 2));
        $g = hexdec(substr($hex, 2, 2));
        $b = hexdec(substr($hex, 4, 2));
        $alpha = (int)(127 * (100 - $opacity) / 100);
        return imagecolorallocatealpha(null, $r, $g, $b, $alpha);
    }
}
?>

使用範例

<?php
$watermarker = new Watermarker([
    'jpegQuality' => 95,
    'maxSize' => 20 * 1024 * 1024,
]);

// 1. 文字浮水印
$watermarker->watermark('photo.jpg', 'photo_wm.jpg', [
    'type' => 'text',
    'content' => '© 2024 MySite.com',
    'position' => 'bottom-right',
    'opacity' => 40,
    'fontSize' => 24,
    'fontColor' => '#FFFFFF',
    'fontFile' => '/fonts/NotoSansTC-Bold.otf',
    'rotation' => -15,
]);

// 2. 圖片浮水印 (Logo)
$watermarker->watermark('photo.jpg', 'photo_logo.jpg', [
    'type' => 'image',
    'content' => '/assets/logo.png',
    'position' => 'center',
    'opacity' => 30,
]);

// 3. 批次處理
$results = $watermarker->batchWatermark('/uploads/originals', '/uploads/watermarked', [
    'type' => 'text',
    'content' => 'Internal Use Only',
    'position' => 'center',
    'opacity' => 20,
    'fontSize' => 36,
    'fontColor' => '#FF0000',
    'rotation' => 45,
]);
?>

上傳整合:自動加浮水印

<?php
// upload.php
$watermarker = new Watermarker();

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
    $file = $_FILES['image'];
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    
    if (!in_array($ext, $allowed)) {
        die('不允許的格式');
    }

    $tempName = $file['tmp_name'];
    $finalName = 'img_' . bin2hex(random_bytes(8)) . '.' . $ext;
    $finalPath = '/uploads/' . $finalName;
    $wmPath = '/uploads/wm_' . $finalName;

    // 移動上傳檔
    move_uploaded_file($tempName, $finalPath);

    // 加浮水印
    try {
        $watermarker->watermark($finalPath, $wmPath, [
            'type' => 'text',
            'content' => '© MySite.com',
            'position' => 'bottom-right',
            'opacity' => 50,
        ]);
        echo json_encode(['success' => true, 'original' => $finalPath, 'watermarked' => $wmPath]);
    } catch (Exception $e) {
        echo json_encode(['success' => false, 'error' => $e->getMessage()]);
    }
}
?>

效能優化建議

場景優化方案
高併發上傳隊列異步處理 + Worker 進程
大量批次Imagick (C 擴展) 比 GD 快 3-5 倍
重複浮水印預先渲染浮水印層快取,合成時直接 copy
超大圖片分塊處理或使用 VIPS (libvips)

浮水印核心在於「透明圖層疊加 + 位置計算 + 格式保真」,GD 滿足大多數需求,高效能場景建議遷移至 Imagick 或 VIPS。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved