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

PHP 圖片加水印完整實作教學:從 GD 庫配置到自動化批次處理

     編輯:PHP 相關知識

前言:為什麼需要自動化加水印?

在開發圖片分享平台、電商系統或內容管理系統時,保護圖片版權、宣傳品牌標識都是必要需求。若每張圖片都靠人工在 Photoshop 中添加水印,不僅效率低下,還難以維持一致性。PHP 內建的 GD 庫提供了強大的圖像處理能力,讓我們能在上傳、生成縮圖或輸出時自動合成水印,實現「上傳即保護」的工作流。

環境準備:啟用 GD 擴展

執行任何圖像操作前,必須確認 PHP 已載入 GD 擴展。打開 php.ini,找到以下行並移除前面的分號:

;extension=php_gd2.dll
→ extension=php_gd2.dll

同時確認 extension_dir 指向正確的擴展目錄。重啟 Apache 或 PHP-FPM 後,透過 phpinfo() 或 extension_loaded('gd') 驗證是否生效。

核心參數設計

一個彈性的水印類別通常需要以下配置:

  • 允許上傳的 MIME 類型:JPEG、PNG、GIF、BMP、WebP 等
  • 檔案大小限制:預設 2 MB,可依業務調整
  • 儲存路徑:建議放在公開目錄外,或以亂數檔名存放
  • 水印類型:文字(版權聲明、網址)或圖片(Logo、QR Code)
  • 水印位置:九宮格任意角落、置中、隨機偏移
  • 透明度與字體大小:平衡可見度與美觀
  • 是否產生縮圖:前台預覽用

完整程式碼範例(物件導向重構版)

<?php
class ImageWatermarker {
    private $config = [
        'allowed_types' => ['image/jpeg','image/png','image/gif','image/webp'],
        'max_size'      => 2_000_000,
        'dest_dir'      => __DIR__ . '/uploads/',
        'watermark'     => true,
        'wm_type'       => 'text',        // 'text' | 'image'
        'wm_position'   => 5,             // 1-9 九宮格
        'wm_text'       => '© MySite.com',
        'wm_font'       => __DIR__ . '/fonts/noto.ttc',
        'wm_font_size'  => 20,
        'wm_color'      => [255,255,255,100], // RGBA
        'wm_image'      => __DIR__ . '/assets/logo.png',
        'wm_opacity'    => 50,            // 0-100
        'make_thumb'    => true,
        'thumb_ratio'   => 0.5,
    ];

    public function __construct(array $overrides = []) {
        $this->config = array_merge($this->config, $overrides);
        if (!is_dir($this->config['dest_dir'])) {
            mkdir($this->config['dest_dir'], 0755, true);
        }
    }

    public function handleUpload(array $file): array {
        // 1. 基礎驗證
        if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
            throw new RuntimeException('無效的上傳檔案');
        }
        if ($file['size'] > $this->config['max_size']) {
            throw new RuntimeException('檔案超過大小限制');
        }
        if (!in_array($file['type'], $this->config['allowed_types'], true)) {
            throw new RuntimeException('不支援的檔案類型:' . $file['type']);
        }

        // 2. 取得圖片資訊
        [$src_w, $src_h, $type] = getimagesize($file['tmp_name']);
        $ext = image_type_to_extension($type, false); // jpeg, png, gif, webp

        // 3. 建立唯一檔名
        $filename = bin2hex(random_bytes(8)) . '.' . $ext;
        $dest_path = $this->config['dest_dir'] . $filename;

        // 4. 依類型建立圖片資源
        $src = $this->createImageFromFile($file['tmp_name'], $type);
        if (!$src) throw new RuntimeException('無法讀取圖片資源');

        // 5. 若啟用水印 → 合成
        if ($this->config['watermark']) {
            $this->applyWatermark($src, $src_w, $src_h);
        }

        // 6. 輸出覆蓋原檔(或另存新檔)
        $this->saveImage($src, $dest_path, $type);

        // 7. 產生縮圖
        $thumb_path = null;
        if ($this->config['make_thumb']) {
            $thumb_path = $this->makeThumbnail($src, $src_w, $src_h, $dest_path, $type);
        }

        imagedestroy($src);
        return [
            'path'      => $dest_path,
            'url'       => '/uploads/' . $filename,
            'width'     => $src_w,
            'height'    => $src_h,
            'size'      => $file['size'],
            'thumb_url' => $thumb_path ? '/uploads/' . basename($thumb_path) : null,
        ];
    }

    private function createImageFromFile(string $path, int $type): ?\GdImage {
        return match ($type) {
            IMAGETYPE_JPEG  => imagecreatefromjpeg($path),
            IMAGETYPE_PNG   => imagecreatefrompng($path),
            IMAGETYPE_GIF   => imagecreatefromgif($path),
            IMAGETYPE_WEBP  => imagecreatefromwebp($path),
            default         => null,
        };
    }

    private function applyWatermark(\GdImage &$canvas, int $w, int $h): void {
        if ($this->config['wm_type'] === 'text') {
            $this->drawTextWatermark($canvas, $w, $h);
        } elseif ($this->config['wm_type'] === 'image' && file_exists($this->config['wm_image'])) {
            $this->drawImageWatermark($canvas, $w, $h);
        }
    }

    private function drawTextWatermark(\GdImage $im, int $w, int $h): void {
        [$r,$g,$b,$a] = $this->config['wm_color'];
        $color = imagecolorallocatealpha($im, $r, $g, $b, 127 - (int)($a * 1.27)); // 0-127
        $box = imagettfbbox($this->config['wm_font_size'], 0, $this->config['wm_font'], $this->config['wm_text']);
        $tw = $box[2] - $box[0];
        $th = $box[1] - $box[7];
        [$x, $y] = $this->calcPosition($w, $h, $tw, $th);
        imagettftext($im, $this->config['wm_font_size'], 0, $x, $y, $color, $this->config['wm_font'], $this->config['wm_text']);
    }

    private function drawImageWatermark(\GdImage $im, int $w, int $h): void {
        $wm = $this->createImageFromFile($this->config['wm_image'], exif_imagetype($this->config['wm_image']));
        if (!$wm) return;
        $wm_w = imagesx($wm);
        $wm_h = imagesy($wm);
        [$x, $y] = $this->calcPosition($w, $h, $wm_w, $wm_h);
        imagecopymerge($im, $wm, $x, $y, 0, 0, $wm_w, $wm_h, $this->config['wm_opacity']);
        imagedestroy($wm);
    }

    private function calcPosition(int $W, int $H, int $w, int $h): array {
        $pad = 10;
        $pos = $this->config['wm_position'];
        $x = match (true) {
            in_array($pos, [1,4,7]) => $pad,
            in_array($pos, [3,6,9]) => $W - $w - $pad,
            default                 => ($W - $w) / 2,
        };
        $y = match (true) {
            in_array($pos, [1,2,3]) => $pad + $h,
            in_array($pos, [7,8,9]) => $H - $pad,
            default                 => ($H + $h) / 2,
        };
        return [(int)$x, (int)$y];
    }

    private function saveImage(\GdImage $im, string $path, int $type): void {
        switch ($type) {
            case IMAGETYPE_JPEG:  imagejpeg($im, $path, 90); break;
            case IMAGETYPE_PNG:   imagepng($im, $path, 6); break;
            case IMAGETYPE_GIF:   imagegif($im, $path); break;
            case IMAGETYPE_WEBP:  imagewebp($im, $path, 90); break;
        }
    }

    private function makeThumbnail(\GdImage $src, int $w, int $h, string $orig_path, int $type): ?string {
        $new_w = (int)($w * $this->config['thumb_ratio']);
        $new_h = (int)($h * $this->config['thumb_ratio']);
        $thumb = imagecreatetruecolor($new_w, $new_h);
        // 保留透明度
        if (in_array($type, [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP], true)) {
            imagealphablending($thumb, false);
            imagesavealpha($thumb, true);
        }
        imagecopyresampled($thumb, $src, 0, 0, 0, 0, $new_w, $new_h, $w, $h);
        $thumb_path = preg_replace('/\.[^.]+$/', '_thumb.$0', $orig_path);
        $this->saveImage($thumb, $thumb_path, $type);
        imagedestroy($thumb);
        return $thumb_path;
    }
}

// ── 使用範例 ──
$wm = new ImageWatermarker([
    'wm_text' => '© ' . date('Y') . ' MyBrand.com',
    'wm_position' => 9, // 右下角
]);
try {
    $result = $wm->handleUpload($_FILES['image']);
    echo json_encode(['success' => true, 'data' => $result]);
} catch (Throwable $e) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}

進階技巧與常見坑

1. 透明度處理

PNG / WebP 含 Alpha 通道時,務必在建立畫布後啟用 imagesavealpha($im, true) 並關閉 imagealphablending($im, false),否則透明區域會變黑。

2. 字體路徑

imagettftext 需要 TrueType 字體檔(.ttf / .ttc)。中文建議使用 Noto Sans TC、思源黑體等開源字體,並以絕對路徑載入。

3. 記憶體限制

大圖(如 4000×3000)處理時可能觸發 memory_limit。可在處理前暫時調大:ini_set('memory_limit', '256M'),或改用 ImageMagick(imagick 擴展)以串流方式處理。

4. 安全性

  • 上傳目錄禁止腳本執行權限(Nginx/Apache 設定 location ~ \.php$ { deny all; })
  • 檔名採用亂數 + 雜湊,避免路徑遍歷
  • 驗證 MIME 型態時結合 finfo_file 二進位偵測,不要只信 $_FILES['type']

總結

透過封裝成可複用的 ImageWatermarker 類別,我們不只解決「加水印」這個單一需求,更建立了可擴充的圖片處理管線:驗證 → 轉檔 → 水印 → 縮圖 → 儲存 → 回傳結構化資訊。未來若需加入「壓縮優化」、「EXIF 剝離」、「雲端同步」等步驟,只需在管線中插入新方法即可,符合單一職責與開放封閉原則。

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