最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Python+PyQt5編寫一個(gè)批量圖片添加水印工具(附源碼)

 更新時(shí)間:2026年02月25日 08:39:44   作者:小莊-Python辦公  
這篇文章主要為大家詳細(xì)介紹了如何基于PyQt5開發(fā)的批量圖片水印添加工具,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

這篇文章完整記錄一個(gè)「批量圖片添加水印」桌面工具的研發(fā)過程:左側(cè)配置水印參數(shù),右側(cè)實(shí)時(shí)預(yù)覽效果,支持單張?zhí)幚砼c文件夾批量處理,適合用作 PyQt5 入門到實(shí)戰(zhàn)的小項(xiàng)目。

1. 需求與界面目標(biāo)

目標(biāo)界面(和你提供的截圖一致的交互形態(tài)):

  • 左側(cè)「水印參數(shù)配置」
    • 水印文字(可編輯)
    • 字體大?。ㄏ袼兀?/li>
    • 透明度(0-100)
    • 文字顏色(彈窗取色)
    • 水印位置(左上/右上/左下/右下/居中)
    • 全屏水印模式(忽略位置,按間距平鋪)
    • 水印旋轉(zhuǎn)角度
    • 水平/垂直間距
    • 行數(shù)/列數(shù)(可選,輔助控制密度)
    • 兩個(gè)按鈕:處理單個(gè)圖片 / 批量處理文件夾
  • 右側(cè)「預(yù)覽」
    • 選擇預(yù)覽圖片后,參數(shù)變化即時(shí)刷新預(yù)覽
  • 批量處理結(jié)束時(shí)彈出提示:成功處理圖片數(shù)量

1.1 效果截圖

你可以把截圖放到項(xiàng)目目錄下(例如 assets/ui.png),然后在博客里引用:

![批量圖片加水印工具-界面預(yù)覽](assets/ui.png)

2. 技術(shù)選型與依賴

2.1 GUI:PyQt5

使用 PyQt5 的原因:

  • 組件完善(輸入框、下拉框、取色對(duì)話框、文件選擇對(duì)話框等一應(yīng)俱全)
  • QPainter 繪制文字水印非常順手
  • 跨平臺(tái)(Windows/macOS/Linux)

2.2 圖片處理:QImage + QPainter

核心思路:把原圖讀入為 QImage,創(chuàng)建一個(gè)同尺寸畫布,在畫布上先繪制原圖,再繪制水印文字,最后保存到目標(biāo)路徑。

3. 工程結(jié)構(gòu)(單文件也能清晰)

這個(gè)項(xiàng)目以單文件實(shí)現(xiàn)為主,代碼結(jié)構(gòu)仍然保持「可擴(kuò)展」:

  • WatermarkSettings:水印配置數(shù)據(jù)模型(dataclass)
  • MainWindow:界面與交互
    • _build_ui():搭界面
    • _wire_signals():綁定信號(hào),實(shí)現(xiàn)“改參數(shù)即刷新預(yù)覽”
    • _apply_watermark():把水印繪制到圖片上
    • _process_single_image():?jiǎn)螆D處理
    • _process_folder():文件夾批量處理

4. 界面實(shí)現(xiàn):左配右預(yù)覽

4.1 左側(cè)參數(shù)配置區(qū)

左側(cè)用一個(gè) QGroupBox("水印參數(shù)配置") 包起來,內(nèi)部用 QVBoxLayout 縱向堆疊每一行配置項(xiàng),每一行再用 QHBoxLayout 實(shí)現(xiàn)“標(biāo)簽 + 控件”的排列。

典型控件選擇:

  • 文本:QLineEdit
  • 數(shù)值:QSpinBox(整數(shù))、QDoubleSpinBox(旋轉(zhuǎn)角度)
  • 下拉:QComboBox
  • 開關(guān):QCheckBox
  • 取色:QColorDialog

對(duì)應(yīng)實(shí)現(xiàn)位置:

  • MainWindow._build_ui():創(chuàng)建控件并組裝布局

4.2 右側(cè)預(yù)覽區(qū)

右側(cè)同樣用 QGroupBox("預(yù)覽") 包起來,核心是一個(gè) QLabel 作為畫布:

  • 通過 QPixmap.fromImage()QImage 轉(zhuǎn)為 QPixmap
  • 使用 scaled(..., Qt.KeepAspectRatio, Qt.SmoothTransformation) 自適應(yīng)縮放

預(yù)覽邏輯在:

  • MainWindow._update_preview()
  • 并在窗口 resizeEvent 里觸發(fā)刷新,保證拖動(dòng)窗口大小時(shí)預(yù)覽不變形

5. 關(guān)鍵交互:參數(shù)變化即刷新預(yù)覽

思路很簡(jiǎn)單:把所有“會(huì)影響預(yù)覽”的控件信號(hào)都綁定到 _update_preview()

在代碼里使用了一個(gè)小技巧:遍歷控件列表,按控件可能擁有的信號(hào)類型去連接:

  • 文本框:textChanged
  • SpinBox:valueChanged
  • ComboBox:currentTextChanged
  • CheckBox:toggled

對(duì)應(yīng)實(shí)現(xiàn)位置:MainWindow._wire_signals()

這樣新增控件也方便:只要把控件加進(jìn)列表,就能自動(dòng)獲得“聯(lián)動(dòng)預(yù)覽”能力。

6. 水印渲染核心:QPainter 怎么畫文字水印

水印繪制主要分兩類:

  • 單個(gè)水?。ò次恢美L制一次)
  • 全屏水印(按間距平鋪繪制多次)

它們都共享同一段“準(zhǔn)備畫筆”的邏輯:

  • 轉(zhuǎn)成 QImage.Format_ARGB32,確保支持透明度混合
  • painter.setOpacity(opacity/100) 設(shè)置整體透明度
  • 設(shè)置字體像素大小 font.setPixelSize(font_size)
  • QPen(QColor) 設(shè)置文字顏色

對(duì)應(yīng)實(shí)現(xiàn)位置:MainWindow._apply_watermark()

6.1 單個(gè)水印:位置 + 旋轉(zhuǎn)

單個(gè)水印的關(guān)鍵點(diǎn):

  • 先用 QFontMetricsF 測(cè)量文字矩形(用于右下等位置的對(duì)齊)
  • 通過 painter.translate(...) 把坐標(biāo)系移動(dòng)到文字中心
  • painter.rotate(angle) 旋轉(zhuǎn)
  • 在“旋轉(zhuǎn)后的坐標(biāo)系”里繪制文字

對(duì)應(yīng)實(shí)現(xiàn)位置:MainWindow._draw_single()

6.2 全屏平鋪水?。浩戒伔秶c密度控制

平鋪水印的關(guān)鍵點(diǎn):

  • 把坐標(biāo)系移動(dòng)到圖片中心再旋轉(zhuǎn),這樣平鋪更自然
  • 使用 spacing_x / spacing_y 控制密度
  • 可選 rows / cols:當(dāng)用戶指定行列數(shù)時(shí),用圖片寬高反推間距
  • 平鋪范圍用 [-w, w]、[-h, h],保證旋轉(zhuǎn)后邊角仍覆蓋

對(duì)應(yīng)實(shí)現(xiàn)位置:MainWindow._draw_tiled()

7. 處理單圖與批量處理:文件選擇與輸出規(guī)則

7.1 處理單圖

流程:

  • 彈出文件選擇框選圖
  • 讀取 QImage
  • 調(diào)用水印渲染
  • 保存為同目錄 *_watermarked.ext
  • 彈窗提示 “已生成 1 張圖片”

對(duì)應(yīng)實(shí)現(xiàn)位置:MainWindow._process_single_image()

7.2 批量處理文件夾

流程:

  • 選擇文件夾
  • 遞歸掃描圖片文件擴(kuò)展名:.jpg/.jpeg/.png/.bmp/.webp
  • 輸出目錄:<選擇的文件夾>/watermarked_output/
  • 逐張保存,同名覆蓋輸出目錄下同名文件
  • 彈窗提示成功處理數(shù)量

對(duì)應(yīng)實(shí)現(xiàn)位置:

  • iter_image_files()
  • MainWindow._process_folder()

8. 如何運(yùn)行

8.1 安裝依賴

pip install PyQt5

8.2 啟動(dòng)程序

在項(xiàng)目目錄執(zhí)行:

python main.py

9. 常見問題(FAQ)

9.1 為什么有些圖片保存失?。?/h3>

可能原因:

  • 圖片路徑包含特殊字符導(dǎo)致保存權(quán)限不足(建議輸出到有寫權(quán)限的目錄)
  • 圖片格式不支持寫入(已支持常見格式;少見格式可先轉(zhuǎn)為 PNG/JPG)

9.2 透明度為什么感覺不明顯?

透明度是對(duì)“整個(gè)繪制操作”生效的,與背景顏色、圖片亮度有關(guān)。深色 圖上淺色文字會(huì)更明顯;如果不明顯,可以:

  • 提高字體大小
  • 調(diào)整顏色對(duì)比
  • 提高透明度(更接近 100)

9.3 全屏水印密度怎么控制?

優(yōu)先級(jí):

  • 行數(shù)/列數(shù) > 0,則用它們反推間距(更直觀)
  • 否則用 水平間距/垂直間距 直接控制

10. 可擴(kuò)展方向

如果你想把它升級(jí)成“更像產(chǎn)品”的工具,推薦從這些方向迭代:

  • 支持圖片縮放后再加水?。ɡ巛敵鼋y(tǒng)一寬度)
  • 支持輸出質(zhì)量/壓縮率(JPG quality)
  • 支持水印陰影/描邊(增強(qiáng)可讀性)
  • 支持導(dǎo)出到自定義目錄,而不是固定 watermarked_output
  • 批量處理加進(jìn)度條與取消按鈕(避免大文件夾卡?。?/li>
  • 支持圖片 EXIF 方向糾正(部分手機(jī)照片會(huì)旋轉(zhuǎn))

11. 打包成可執(zhí)行程序(Windows)

如果你想發(fā)給沒有 Python 環(huán)境的同學(xué)使用,最常見做法是用 PyInstaller 打包。

11.1 安裝 PyInstaller

pip install pyinstaller

11.2 一鍵打包(無控制臺(tái)窗口)

在項(xiàng)目目錄執(zhí)行:

pyinstaller -F -w main.py --name 圖片水印批量工具

打包完成后可執(zhí)行文件通常在 dist/圖片水印批量工具.exe。

11.3 常見打包問題

如果運(yùn)行時(shí)報(bào)缺少 Qt 插件(例如 platform plugin),可以嘗試升級(jí) PyInstaller,或使用:

pyinstaller -F -w main.py --name 圖片水印批量工具 --collect-all PyQt5

12. 關(guān)鍵代碼入口

  • 程序入口:main() -> MainWindow()
  • 預(yù)覽刷新:_update_preview()
  • 水印渲染:_apply_watermark() -> _draw_single() / _draw_tiled()
  • 批量掃描:iter_image_files()

如果你準(zhǔn)備把項(xiàng)目拆成“UI + 業(yè)務(wù) + 工具函數(shù)”的結(jié)構(gòu),也可以在后續(xù)把水印渲染獨(dú)立成一個(gè)模塊(例如 watermark.py),界面只負(fù)責(zé)讀參數(shù)和調(diào)用即可。

13.完整代碼

import os
import sys
from dataclasses import dataclass
from typing import Iterable, Optional

from PyQt5.QtCore import Qt, QPointF
from PyQt5.QtGui import QColor, QFont, QFontMetricsF, QImage, QPainter, QPen, QPixmap
from PyQt5.QtWidgets import (
    QApplication,
    QCheckBox,
    QColorDialog,
    QComboBox,
    QDoubleSpinBox,
    QFileDialog,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QSizePolicy,
    QSpinBox,
    QVBoxLayout,
    QWidget,
)


@dataclass(frozen=True)
class WatermarkSettings:
    text: str
    font_size: int
    opacity: int
    color: QColor
    position: str
    fullscreen: bool
    rotation: float
    spacing_x: int
    spacing_y: int
    rows: int
    cols: int


SUPPORTED_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}


def iter_image_files(folder: str) -> Iterable[str]:
    for root, _, files in os.walk(folder):
        for name in files:
            ext = os.path.splitext(name)[1].lower()
            if ext in SUPPORTED_EXTS:
                yield os.path.join(root, name)


class MainWindow(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("圖片水印批量工具")
        self.resize(1100, 650)

        self._current_image_path: Optional[str] = None
        self._color = QColor(120, 0, 120)

        self._build_ui()
        self._wire_signals()
        self._update_color_button()
        self._update_preview()

    def _build_ui(self) -> None:
        root = QWidget(self)
        self.setCentralWidget(root)

        main_layout = QHBoxLayout(root)

        self.group_config = QGroupBox("水印參數(shù)配置", root)
        cfg_layout = QVBoxLayout(self.group_config)

        row1 = QHBoxLayout()
        row1.addWidget(QLabel("水印文字:", self.group_config))
        self.input_text = QLabel(self.group_config)
        self.input_text.setTextInteractionFlags(Qt.TextSelectableByMouse)
        self.text_value = QLabel(self.group_config)
        self.text_value.hide()
        self.text_edit = None
        from PyQt5.QtWidgets import QLineEdit

        self.text_edit = QLineEdit(self.group_config)
        self.text_edit.setText("@小莊-Python辦公")
        row1.addWidget(self.text_edit, 1)
        cfg_layout.addLayout(row1)

        row2 = QHBoxLayout()
        row2.addWidget(QLabel("字體大?。?, self.group_config))
        self.spin_font = QSpinBox(self.group_config)
        self.spin_font.setRange(6, 400)
        self.spin_font.setValue(40)
        row2.addWidget(self.spin_font)
        row2.addSpacing(20)
        row2.addWidget(QLabel("透明度(0-100):", self.group_config))
        self.spin_opacity = QSpinBox(self.group_config)
        self.spin_opacity.setRange(0, 100)
        self.spin_opacity.setValue(80)
        row2.addWidget(self.spin_opacity)
        row2.addStretch(1)
        cfg_layout.addLayout(row2)

        row3 = QHBoxLayout()
        row3.addWidget(QLabel("文字顏色:", self.group_config))
        self.btn_color = QPushButton("選擇顏色", self.group_config)
        self.btn_color.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        self.color_swatch = QLabel(self.group_config)
        self.color_swatch.setFixedSize(34, 22)
        self.color_swatch.setFrameShape(QLabel.Box)
        row3.addWidget(self.btn_color)
        row3.addWidget(self.color_swatch)
        row3.addStretch(1)
        cfg_layout.addLayout(row3)

        row4 = QHBoxLayout()
        row4.addWidget(QLabel("水印位置:", self.group_config))
        self.combo_pos = QComboBox(self.group_config)
        self.combo_pos.addItems(["左上", "右上", "左下", "右下", "居中"])
        self.combo_pos.setCurrentText("右下")
        row4.addWidget(self.combo_pos)
        row4.addStretch(1)
        cfg_layout.addLayout(row4)

        row5 = QHBoxLayout()
        self.chk_fullscreen = QCheckBox("全屏水印模式(忽略位置設(shè)置)", self.group_config)
        self.chk_fullscreen.setChecked(False)
        row5.addWidget(self.chk_fullscreen)
        row5.addStretch(1)
        cfg_layout.addLayout(row5)

        row6 = QHBoxLayout()
        row6.addWidget(QLabel("水印旋轉(zhuǎn)角度:", self.group_config))
        self.spin_rotation = QDoubleSpinBox(self.group_config)
        self.spin_rotation.setRange(-180.0, 180.0)
        self.spin_rotation.setDecimals(1)
        self.spin_rotation.setSingleStep(1.0)
        self.spin_rotation.setValue(30.0)
        row6.addWidget(self.spin_rotation)
        row6.addStretch(1)
        cfg_layout.addLayout(row6)

        row7 = QHBoxLayout()
        row7.addWidget(QLabel("水平間距(像素):", self.group_config))
        self.spin_spacing_x = QSpinBox(self.group_config)
        self.spin_spacing_x.setRange(20, 5000)
        self.spin_spacing_x.setValue(360)
        row7.addWidget(self.spin_spacing_x)
        row7.addSpacing(20)
        row7.addWidget(QLabel("垂直間距(像素):", self.group_config))
        self.spin_spacing_y = QSpinBox(self.group_config)
        self.spin_spacing_y.setRange(20, 5000)
        self.spin_spacing_y.setValue(200)
        row7.addWidget(self.spin_spacing_y)
        row7.addStretch(1)
        cfg_layout.addLayout(row7)

        row8 = QHBoxLayout()
        row8.addWidget(QLabel("行數(shù):", self.group_config))
        self.spin_rows = QSpinBox(self.group_config)
        self.spin_rows.setRange(0, 200)
        self.spin_rows.setValue(0)
        row8.addWidget(self.spin_rows)
        row8.addSpacing(20)
        row8.addWidget(QLabel("列數(shù):", self.group_config))
        self.spin_cols = QSpinBox(self.group_config)
        self.spin_cols.setRange(0, 200)
        self.spin_cols.setValue(1)
        row8.addWidget(self.spin_cols)
        row8.addStretch(1)
        cfg_layout.addLayout(row8)

        cfg_layout.addStretch(1)

        btn_row = QHBoxLayout()
        self.btn_single = QPushButton("處理單個(gè)圖片", self.group_config)
        self.btn_folder = QPushButton("批量處理文件夾", self.group_config)
        btn_row.addWidget(self.btn_single)
        btn_row.addWidget(self.btn_folder)
        cfg_layout.addLayout(btn_row)

        main_layout.addWidget(self.group_config, 0)

        self.group_preview = QGroupBox("預(yù)覽", root)
        prev_layout = QVBoxLayout(self.group_preview)
        self.preview = QLabel(self.group_preview)
        self.preview.setAlignment(Qt.AlignCenter)
        self.preview.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.preview.setMinimumSize(520, 520)
        self.preview.setStyleSheet("background: #1f1f1f; color: #dddddd;")
        prev_layout.addWidget(self.preview, 1)

        self.btn_choose_preview = QPushButton("選擇預(yù)覽圖片", self.group_preview)
        prev_layout.addWidget(self.btn_choose_preview, 0, Qt.AlignRight)

        main_layout.addWidget(self.group_preview, 1)

    def _wire_signals(self) -> None:
        self.btn_color.clicked.connect(self._choose_color)
        self.btn_choose_preview.clicked.connect(self._choose_preview_image)
        self.btn_single.clicked.connect(self._process_single_image)
        self.btn_folder.clicked.connect(self._process_folder)

        for w in [
            self.text_edit,
            self.spin_font,
            self.spin_opacity,
            self.combo_pos,
            self.chk_fullscreen,
            self.spin_rotation,
            self.spin_spacing_x,
            self.spin_spacing_y,
            self.spin_rows,
            self.spin_cols,
        ]:
            if hasattr(w, "textChanged"):
                w.textChanged.connect(self._update_preview)
            if hasattr(w, "valueChanged"):
                w.valueChanged.connect(self._update_preview)
            if hasattr(w, "currentTextChanged"):
                w.currentTextChanged.connect(self._update_preview)
            if hasattr(w, "toggled"):
                w.toggled.connect(self._update_preview)

    def _settings(self) -> WatermarkSettings:
        return WatermarkSettings(
            text=self.text_edit.text().strip(),
            font_size=int(self.spin_font.value()),
            opacity=int(self.spin_opacity.value()),
            color=QColor(self._color),
            position=self.combo_pos.currentText(),
            fullscreen=self.chk_fullscreen.isChecked(),
            rotation=float(self.spin_rotation.value()),
            spacing_x=int(self.spin_spacing_x.value()),
            spacing_y=int(self.spin_spacing_y.value()),
            rows=int(self.spin_rows.value()),
            cols=int(self.spin_cols.value()),
        )

    def _choose_color(self) -> None:
        color = QColorDialog.getColor(self._color, self, "選擇水印顏色")
        if color.isValid():
            self._color = color
            self._update_color_button()
            self._update_preview()

    def _update_color_button(self) -> None:
        self.color_swatch.setStyleSheet(
            f"background-color: {self._color.name()}; border: 1px solid #444;"
        )

    def _choose_preview_image(self) -> None:
        path, _ = QFileDialog.getOpenFileName(
            self,
            "選擇圖片",
            "",
            "Images (*.png *.jpg *.jpeg *.bmp *.webp);;All Files (*)",
        )
        if not path:
            return
        self._current_image_path = path
        self._update_preview()

    def _process_single_image(self) -> None:
        src, _ = QFileDialog.getOpenFileName(
            self,
            "選擇要處理的圖片",
            "",
            "Images (*.png *.jpg *.jpeg *.bmp *.webp);;All Files (*)",
        )
        if not src:
            return

        img = QImage(src)
        if img.isNull():
            QMessageBox.warning(self, "錯(cuò)誤", "圖片讀取失敗")
            return

        dst = self._suggest_output_path(src)
        ok = self._save_watermarked(img, dst)
        if not ok:
            QMessageBox.warning(self, "錯(cuò)誤", "圖片保存失敗")
            return

        self._current_image_path = src
        self._update_preview()
        QMessageBox.information(self, "批量完成", "處理成功,已生成 1 張圖片")

    def _process_folder(self) -> None:
        folder = QFileDialog.getExistingDirectory(self, "選擇要批量處理的文件夾", "")
        if not folder:
            return

        out_dir = os.path.join(folder, "watermarked_output")
        os.makedirs(out_dir, exist_ok=True)

        count = 0
        first_image = None
        for src in iter_image_files(folder):
            if os.path.commonpath([src, out_dir]) == out_dir:
                continue
            if first_image is None:
                first_image = src

            img = QImage(src)
            if img.isNull():
                continue

            base = os.path.basename(src)
            dst = os.path.join(out_dir, base)
            if self._save_watermarked(img, dst):
                count += 1

        if first_image:
            self._current_image_path = first_image
            self._update_preview()

        QMessageBox.information(self, "批量完成", f"批量處理結(jié)束!\n共成功處理 {count} 張圖片")

    def _suggest_output_path(self, src: str) -> str:
        root, ext = os.path.splitext(src)
        return f"{root}_watermarked{ext}"

    def _save_watermarked(self, src_img: QImage, dst: str) -> bool:
        settings = self._settings()
        if not settings.text:
            return False
        out = self._apply_watermark(src_img, settings)
        os.makedirs(os.path.dirname(dst), exist_ok=True)
        return out.save(dst)

    def _apply_watermark(self, src_img: QImage, settings: WatermarkSettings) -> QImage:
        if src_img.format() != QImage.Format_ARGB32:
            base = src_img.convertToFormat(QImage.Format_ARGB32)
        else:
            base = QImage(src_img)

        out = QImage(base.size(), QImage.Format_ARGB32)
        out.fill(Qt.transparent)

        painter = QPainter(out)
        painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
        painter.drawImage(0, 0, base)

        font = QFont()
        font.setPixelSize(settings.font_size)
        painter.setFont(font)

        pen = QPen(settings.color)
        painter.setPen(pen)
        painter.setOpacity(max(0.0, min(1.0, settings.opacity / 100.0)))

        if settings.fullscreen:
            self._draw_tiled(painter, out.width(), out.height(), settings)
        else:
            self._draw_single(painter, out.width(), out.height(), settings)

        painter.end()
        return out

    def _draw_single(self, painter: QPainter, w: int, h: int, settings: WatermarkSettings) -> None:
        metrics = QFontMetricsF(painter.font())
        rect = metrics.boundingRect(settings.text)
        margin = 20.0

        if settings.position == "左上":
            x = margin
            y = margin + rect.height()
        elif settings.position == "右上":
            x = w - margin - rect.width()
            y = margin + rect.height()
        elif settings.position == "左下":
            x = margin
            y = h - margin
        elif settings.position == "右下":
            x = w - margin - rect.width()
            y = h - margin
        else:
            x = (w - rect.width()) / 2.0
            y = (h + rect.height()) / 2.0

        painter.save()
        painter.translate(x + rect.width() / 2.0, y - rect.height() / 2.0)
        painter.rotate(settings.rotation)
        painter.drawText(QPointF(-rect.width() / 2.0, rect.height() / 2.0), settings.text)
        painter.restore()

    def _draw_tiled(self, painter: QPainter, w: int, h: int, settings: WatermarkSettings) -> None:
        metrics = QFontMetricsF(painter.font())
        rect = metrics.boundingRect(settings.text)

        spacing_x = max(20, settings.spacing_x)
        spacing_y = max(20, settings.spacing_y)
        if settings.cols > 0:
            spacing_x = max(20, int(w / max(1, settings.cols)))
        if settings.rows > 0:
            spacing_y = max(20, int(h / max(1, settings.rows)))

        painter.save()
        painter.translate(w / 2.0, h / 2.0)
        painter.rotate(settings.rotation)

        start_x = -w
        end_x = w
        start_y = -h
        end_y = h

        x = start_x
        while x <= end_x:
            y = start_y
            while y <= end_y:
                painter.drawText(QPointF(x, y), settings.text)
                y += spacing_y
            x += spacing_x

        painter.restore()

    def _update_preview(self) -> None:
        if not self._current_image_path:
            self.preview.setText("請(qǐng)選擇預(yù)覽圖片")
            return

        img = QImage(self._current_image_path)
        if img.isNull():
            self.preview.setText("預(yù)覽圖片讀取失敗")
            return

        settings = self._settings()
        if not settings.text:
            rendered = img
        else:
            rendered = self._apply_watermark(img, settings)

        pix = QPixmap.fromImage(rendered)
        target = self.preview.size()
        if target.width() <= 1 or target.height() <= 1:
            self.preview.setPixmap(pix)
            return
        self.preview.setPixmap(pix.scaled(target, Qt.KeepAspectRatio, Qt.SmoothTransformation))

    def resizeEvent(self, event) -> None:
        super().resizeEvent(event)
        self._update_preview()


def main() -> int:
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    return app.exec_()


if __name__ == "__main__":
    raise SystemExit(main())


以上就是Python+PyQt5編寫一個(gè)批量圖片添加水印工具(附源碼)的詳細(xì)內(nèi)容,更多關(guān)于Python圖片添加水印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Python3列表刪除的三種方式實(shí)現(xiàn)

    Python3列表刪除的三種方式實(shí)現(xiàn)

    本文主要介紹了Python3列表刪除的三種方式實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-08-08
  • Python如何在ubuntu中更改Python和pip指向

    Python如何在ubuntu中更改Python和pip指向

    這篇文章主要介紹了Python如何在ubuntu中更改Python和pip指向問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • 一文詳解Python的re.match使用方法和技巧

    一文詳解Python的re.match使用方法和技巧

    在Python文本處理中,正則表達(dá)式是不可或缺的利器,本文基于2025年最新實(shí)踐,結(jié)合阿里云等權(quán)威平臺(tái)的技術(shù)文檔,系統(tǒng)解析re.match()的核心用法、高級(jí)技巧與常見陷阱,助您精準(zhǔn)駕馭字符串匹配藝術(shù),需要的朋友可以參考下
    2025-11-11
  • 利用Python?爬取股票實(shí)時(shí)數(shù)據(jù)詳情

    利用Python?爬取股票實(shí)時(shí)數(shù)據(jù)詳情

    這篇文章主要介紹了利用Python?爬取股票實(shí)時(shí)數(shù)據(jù)詳情,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下
    2022-08-08
  • Python讀取pdf文件的簡(jiǎn)單代碼示例

    Python讀取pdf文件的簡(jiǎn)單代碼示例

    PDF文件的數(shù)據(jù)主要是文本、圖片、表格,這三部分組成,但是也會(huì)穿插流程圖、各種柱狀圖等,這篇文章主要給大家介紹了關(guān)于Python讀取pdf文件的簡(jiǎn)單代碼示例,需要的朋友可以參考下
    2024-02-02
  • python實(shí)現(xiàn)壁紙批量下載代碼實(shí)例

    python實(shí)現(xiàn)壁紙批量下載代碼實(shí)例

    本篇文章通過代碼實(shí)例給大家分享了用python實(shí)現(xiàn)壁紙批量下載這個(gè)功能,有興趣的朋友參考下吧。
    2018-01-01
  • Python實(shí)現(xiàn)爬取騰訊招聘網(wǎng)崗位信息

    Python實(shí)現(xiàn)爬取騰訊招聘網(wǎng)崗位信息

    這篇文章主要介紹了如何用python爬取騰訊招聘網(wǎng)崗位信息保存到表格,并做成簡(jiǎn)單可視化。文中的示例代碼對(duì)學(xué)習(xí)Python有一定的幫助,感興趣的可以了解一下
    2022-01-01
  • pandas中DataFrame字典互轉(zhuǎn)的實(shí)現(xiàn)

    pandas中DataFrame字典互轉(zhuǎn)的實(shí)現(xiàn)

    在數(shù)據(jù)處理和分析中,Pandas是一個(gè)非常強(qiáng)大的Python庫,本文主要介紹了pandas中DataFrame字典互轉(zhuǎn)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-04-04
  • 11個(gè)并不被常用但對(duì)開發(fā)非常有幫助的Python庫

    11個(gè)并不被常用但對(duì)開發(fā)非常有幫助的Python庫

    這篇文章主要介紹了11個(gè)并不被常用但對(duì)開發(fā)非常有幫助的Python庫,這些庫大都被放在Github上開源、并且經(jīng)過一段時(shí)間的編寫和維護(hù),對(duì)Python開發(fā)有一定的幫助,需要的朋友可以參考下
    2015-03-03
  • 基于Python編寫圖片去水印小工具

    基于Python編寫圖片去水印小工具

    你有沒有注意到,某魚上有不少提供去水印的服務(wù),收取1塊錢每張圖,最搞笑的是,很多人都沒意識(shí)到,去水印的技術(shù)其實(shí)非常簡(jiǎn)單,所以本文來和大家講講如何使用Python編寫圖片去水印小工具吧
    2025-05-05

最新評(píng)論

泰顺县| 曲阜市| 桂林市| 萨嘎县| 揭阳市| 无极县| 敦煌市| 资溪县| 清流县| 贡嘎县| 东台市| 错那县| 淮安市| 云南省| 福清市| 泾阳县| 林芝县| 城步| 泸定县| 扎鲁特旗| 思茅市| 七台河市| 重庆市| 平顺县| 临朐县| 德阳市| 元江| 柳河县| 福清市| 临颍县| 雅安市| 久治县| 白沙| 手游| 房产| 库伦旗| 镇江市| 通化市| 金山区| 遂昌县| 冀州市|