吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2484|回复: 41
收起左侧

[Python 原创] PDF加水印工具

  [复制链接]
人间值得 发表于 2026-4-15 15:08
一、这工具是从哪儿来的

这个工具最早是基于吾爱破解论坛上 **pythonfun** 大佬发的帖子做的,原帖地址在这儿:  
[https://www.52pojie.cn/forum.php?mod=viewthread&tid=2058289&highlight=pdf%2B%BC%D3%2B%CB%AE%D3%A1](https://www.52pojie.cn/forum.php?mod=viewthread&tid=2058289&highlight=pdf%2B%BC%D3%2B%CB%AE%D3%A1)

原版其实已经能用,但我自己拿过来用的时候踩了几个坑,加上平时工作里用起来总觉得差点意思,就顺手改了改。

二、为啥我要改它

因为工作上的需求
- 有时候就只想给一个 PDF 加水印,原版只能选文件夹,略麻烦。
- 需要水印能换行(比如写两行公司名和日期)。
- 有些 PDF 页面尺寸不一样,固定字号容易太大太小,想要个按比例自动调字号的功能。
- 偶尔需要水印稍微偏一点位置,或者只给前几页加。

所以把代码整个翻了一遍,加了一堆自己需要的功能。

三、我都改了些什么

- 字体自动扫描:打开软件会自动扫一遍系统里的中文字体,列出来让你选。Windows 和 macOS 都能用,不用自己去翻字体文件路径。
- 可以只处理单个文件:界面顶上可以选“处理单个文件”还是“处理文件夹”。输出文件夹不填的话,水印后的文件会直接放在原 PDF 旁边,省得再选一次。
- 水印可以换行:文本框里直接回车换行,打出来的水印就是多行的,而且会自动居中。
- 字号允许按照页面比例设置:勾上“相对页面比例”,水印大小就会根据 PDF 页面短边的长度自动缩放,再也不用担心小 PDF 水印太大、大 PDF 水印太小的问题。
- 水印位置允许任意调整:多了水平和垂直的偏移设置,单位是毫米,微调很方便。
- 允许指定页面增加水印:可以选“全部页面”“仅首页”或者自己填页码范围,比如 `1-3,5`。

界面也稍微收拾了一下,把设置分到了几个标签页里,看起来没那么挤。

四、大概怎么用

1. 打开软件,选“处理单个文件”还是“文件夹”。
2. 选好 PDF 文件(或文件夹),输出路径不填的话就默认存到原文件旁边。
3. 在“基本设置”里把要打的水印文字写上,需要换行就直接回车。
4. 字体下拉框里选一个中文字体,没找到的话点一下“刷新字体”。
5. 其他旋转角度、透明度、间距什么的按自己需要调。
6. 点“开始加水印”,等进度条跑完就行了。

五、源代码
[Python] 纯文本查看 复制代码
import os
import sys
import threading
import queue
import platform
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import tkinter.font as tkfont
from reportlab.pdfgen import canvas
from reportlab.lib import colors
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from PyPDF2 import PdfReader, PdfWriter

# ---------- 过滤控制台无关输出 ----------
class FilterStderr:
    def __init__(self, stream):
        self.stream = stream
    def write(self, msg):
        if not any(x in msg for x in ('libpng warning', '生成链路关键节点上报')):
            self.stream.write(msg)
    def flush(self):
        self.stream.flush()

sys.stderr = FilterStderr(sys.stderr)


# ------------------ 字体扫描辅助函数 ------------------
def get_system_font_dirs():
    system = platform.system()
    dirs = []
    if system == 'Windows':
        dirs.append(r'C:\Windows\Fonts')
        username = os.environ.get('USERNAME') or os.environ.get('USER')
        if username:
            user_font_dir = fr'C:\Users\{username}\AppData\Local\Microsoft\Windows\Fonts'
            if os.path.exists(user_font_dir):
                dirs.append(user_font_dir)
        local_appdata = os.environ.get('LOCALAPPDATA')
        if local_appdata:
            dirs.append(os.path.join(local_appdata, 'Microsoft', 'Windows', 'Fonts'))
    elif system == 'Darwin':
        dirs.extend([
            '/System/Library/Fonts',
            '/Library/Fonts',
            os.path.expanduser('~/Library/Fonts')
        ])
    else:
        dirs.extend(['/usr/share/fonts', os.path.expanduser('~/.fonts')])
    return [d for d in dirs if d and os.path.exists(d)]


def get_installed_fonts_from_tk():
    root_tmp = tk.Tk()
    root_tmp.withdraw()
    families = list(tkfont.families(root_tmp))
    root_tmp.destroy()
    return families


def extract_font_family(font_path):
    try:
        if font_path.lower().endswith('.ttc'):
            font = TTFont(font_path, subfontIndex=0, validate=False)
        else:
            font = TTFont(font_path, validate=False)
        return font.face.family_name
    except:
        return None


def scan_available_fonts():
    font_map = {}
    font_dirs = get_system_font_dirs()
    installed_families = get_installed_fonts_from_tk()
    chinese_keywords = ['宋', '黑', '楷', '仿', '雅黑', '苹方', 'song', 'hei', 'kai', 'fang',
                        'microsoft yahei', 'pingfang', 'simsun', 'simhei', 'simkai', 'kaiti', 'songti']

    file_family_map = {}
    for font_dir in font_dirs:
        for root, _, files in os.walk(font_dir):
            for file in files:
                if file.lower().endswith(('.ttf', '.ttc')):
                    full_path = os.path.join(root, file)
                    family = extract_font_family(full_path)
                    if family:
                        file_family_map[full_path] = family
                    elif any(kw in file.lower() for kw in chinese_keywords):
                        file_family_map[full_path] = os.path.splitext(file)[0]

    for family in installed_families:
        family_lower = family.lower()
        if not any(kw in family_lower for kw in chinese_keywords):
            continue
        found_path = None
        for path, fam in file_family_map.items():
            if fam == family or family in fam or fam in family:
                found_path = path
                break
        if not found_path:
            for path in file_family_map:
                file_name = os.path.basename(path).lower()
                if family_lower.replace(' ', '') in file_name.replace(' ', ''):
                    found_path = path
                    break
        if found_path:
            font_map[family] = found_path

    if len(font_map) < 2:
        for path, family in file_family_map.items():
            if any(kw in family.lower() for kw in chinese_keywords) and family not in font_map:
                font_map[family] = path

    if not font_map:
        font_map["默认字体 (无法显示中文)"] = ""
    return font_map


# ------------------ 加水印线程 ------------------
class WatermarkThread(threading.Thread):
    def __init__(self, settings, progress_queue):
        super().__init__()
        self.settings = settings
        self.progress_queue = progress_queue
        self.is_running = True
        self.daemon = True

    def stop_process(self):
        self.is_running = False

    def run(self):
        input_mode = self.settings['input_mode']
        output_folder = self.settings['output_folder']

        if input_mode == 'folder':
            pdf_folder = self.settings['pdf_folder']
            if not pdf_folder or not output_folder:
                self.progress_queue.put((0, "错误:文件夹模式需要指定输出文件夹"))
                return
            pdf_files = [os.path.join(pdf_folder, f) for f in os.listdir(pdf_folder) if f.lower().endswith('.pdf')]
        else:
            pdf_path = self.settings['pdf_file']
            if not pdf_path:
                self.progress_queue.put((0, "错误:未选择 PDF 文件"))
                return
            pdf_files = [pdf_path]
            if not output_folder:
                output_folder = os.path.dirname(pdf_path)
                self.settings['output_folder'] = output_folder

        os.makedirs(output_folder, exist_ok=True)
        total = len(pdf_files)
        processed = 0

        for file_path in pdf_files:
            if not self.is_running:
                self.progress_queue.put((0, "用户中止"))
                return
            processed += 1
            progress = int((processed / total) * 100) if total > 0 else 0
            filename = os.path.basename(file_path)
            self.progress_queue.put((progress, f"正在处理 ({processed}/{total}): {filename}"))
            try:
                out_path = self.add_watermark(file_path)
                self.progress_queue.put((progress, f"完成: {os.path.basename(out_path)}"))
            except Exception as e:
                self.progress_queue.put((progress, f"失败 {filename}: {str(e)}"))
        self.progress_queue.put((100, "所有任务完成!"))

    def add_watermark(self, pdf_path):
        s = self.settings
        reader = PdfReader(pdf_path)
        if len(reader.pages) == 0:
            raise ValueError("PDF 无页面")
        first_page = reader.pages[0]
        mb = first_page.mediabox
        page_width = float(mb.width)
        page_height = float(mb.height)

        if s['use_relative_scale']:
            base_size = min(page_width, page_height) * (s['relative_scale_percent'] / 100.0)
        else:
            base_size = s['font_size']

        font_name = s['font_family']
        font_path = s['font_path']
        if font_path and font_name not in pdfmetrics.getRegisteredFontNames():
            subfont = 0 if font_path.lower().endswith('.ttc') else None
            pdfmetrics.registerFont(TTFont(font_name, font_path, subfontIndex=subfont))

        temp_wm = os.path.join(s['output_folder'], "__temp_watermark.pdf")
        c = canvas.Canvas(temp_wm, pagesize=(page_width, page_height))

        c.setFillColor(colors.black, alpha=s['alpha'])
        if font_name in pdfmetrics.getRegisteredFontNames():
            c.setFont(font_name, base_size)
        else:
            c.setFont("Helvetica", base_size)

        lines = s['watermark_text'].split('\n')
        line_height = base_size * 1.2
        mm_to_pt = 2.83465
        offset_x = s['offset_x_mm'] * mm_to_pt
        offset_y = s['offset_y_mm'] * mm_to_pt

        c.saveState()
        c.translate(page_width / 2 + offset_x, page_height / 2 + offset_y)
        c.rotate(s['angle'])

        row_spacing = s['row_spacing']
        col_spacing = s['col_spacing']

        extend = max(page_width, page_height) * 2
        start_x = -extend
        start_y = -extend
        end_x = extend
        end_y = extend

        x = start_x
        while x < end_x:
            y = start_y
            while y < end_y:
                for i, line in enumerate(lines):
                    total_height = (len(lines) - 1) * line_height
                    y_offset = total_height / 2 - i * line_height
                    c.drawString(x, y + y_offset, line)
                y += row_spacing
            x += col_spacing

        c.restoreState()
        c.save()

        wm_reader = PdfReader(temp_wm)
        writer = PdfWriter()
        page_range = self.parse_page_range(s['page_range'], len(reader.pages))
        for i, page in enumerate(reader.pages):
            if i in page_range:
                page.merge_page(wm_reader.pages[0])
            writer.add_page(page)

        base = os.path.splitext(os.path.basename(pdf_path))[0]
        out_file = os.path.join(s['output_folder'], f"{base}_watermarked.pdf")
        with open(out_file, "wb") as f:
            writer.write(f)
        os.remove(temp_wm)
        return out_file

    def parse_page_range(self, range_str, total_pages):
        if range_str.strip() == "全部页面":
            return set(range(total_pages))
        pages = set()
        for p in range_str.split(','):
            p = p.strip()
            if '-' in p:
                start, end = p.split('-')
                start = int(start) - 1
                end = int(end) - 1
                if start < 0: start = 0
                if end >= total_pages: end = total_pages - 1
                pages.update(range(start, end + 1))
            else:
                idx = int(p) - 1
                if 0 <= idx < total_pages:
                    pages.add(idx)
        return pages


# ------------------ 主界面 ------------------
class PDFWatermarkApp:
    def __init__(self, root):
        self.root = root
        self.watermark_thread = None
        self.progress_queue = queue.Queue()
        self.available_fonts = {}
        self.initUI()
        self.check_queue()
        self.scan_fonts()

    def initUI(self):
        self.root.title('PDF 水印工具 - 专业版')
        self.root.geometry('700x700')
        self.root.minsize(650, 600)

        default_font = tkfont.nametofont("TkDefaultFont")
        default_font.configure(family="微软雅黑" if platform.system() == 'Windows' else "PingFang SC", size=10)
        self.root.option_add("*Font", default_font)

        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 输入模式
        mode_frame = ttk.LabelFrame(main_frame, text="输入模式", padding="5")
        mode_frame.pack(fill=tk.X, pady=5)
        self.input_mode = tk.StringVar(value="single")
        ttk.Radiobutton(mode_frame, text="处理文件夹", variable=self.input_mode, value="folder", command=self.on_mode_change).pack(side=tk.LEFT, padx=10)
        ttk.Radiobutton(mode_frame, text="处理单个文件", variable=self.input_mode, value="single", command=self.on_mode_change).pack(side=tk.LEFT, padx=10)

        self.folder_frame = ttk.LabelFrame(main_frame, text="源文件设置", padding="5")
        self.folder_frame.pack(fill=tk.X, pady=5)

        # 文件夹模式控件
        self.folder_widgets = []
        self.single_widgets = []

        # 文件夹模式
        f1 = ttk.Frame(self.folder_frame)
        ttk.Label(f1, text="PDF 文件夹:", width=12).pack(side=tk.LEFT)
        self.pdf_folder_var = tk.StringVar()
        ttk.Entry(f1, textvariable=self.pdf_folder_var).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Button(f1, text="选择", command=self.browse_pdf_folder).pack(side=tk.RIGHT)
        self.folder_widgets.append(f1)

        # 单文件模式
        f2 = ttk.Frame(self.folder_frame)
        ttk.Label(f2, text="PDF 文件:", width=12).pack(side=tk.LEFT)
        self.pdf_file_var = tk.StringVar()
        ttk.Entry(f2, textvariable=self.pdf_file_var).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Button(f2, text="选择", command=self.browse_pdf_file).pack(side=tk.RIGHT)
        self.single_widgets.append(f2)

        # 输出文件夹
        f3 = ttk.Frame(self.folder_frame)
        ttk.Label(f3, text="输出文件夹:", width=12).pack(side=tk.LEFT)
        self.output_folder_var = tk.StringVar()
        self.output_entry = ttk.Entry(f3, textvariable=self.output_folder_var)  # 确保 output_entry 存在
        self.output_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Button(f3, text="选择", command=self.browse_output_folder).pack(side=tk.RIGHT)
        ttk.Label(f3, text="(留空则与源文件同目录)", foreground="gray").pack(side=tk.LEFT, padx=5)
        f3.pack(fill=tk.X, pady=2)  # 提前 pack,确保在 on_mode_change 之前已布局

        # 现在可以安全调用 on_mode_change
        self.on_mode_change()

        # Notebook 设置
        notebook = ttk.Notebook(main_frame)
        notebook.pack(fill=tk.BOTH, expand=True, pady=5)

        # ---------- 标签页1:基本设置 ----------
        tab1 = ttk.Frame(notebook, padding="10")
        notebook.add(tab1, text="基本设置")

        row = 0
        ttk.Label(tab1, text="水印文字:").grid(row=row, column=0, sticky=tk.W, pady=5)
        self.watermark_var = tk.StringVar(value="安全组评审完了")
        text_frame = ttk.Frame(tab1)
        text_frame.grid(row=row, column=1, columnspan=3, sticky=tk.EW, pady=5)
        self.watermark_entry = tk.Text(text_frame, height=3, width=50, font=default_font)
        self.watermark_entry.insert("1.0", self.watermark_var.get())
        self.watermark_entry.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scroll = ttk.Scrollbar(text_frame, command=self.watermark_entry.yview)
        scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.watermark_entry.config(yscrollcommand=scroll.set)
        tab1.columnconfigure(1, weight=1)

        row += 1
        ttk.Label(tab1, text="字体:").grid(row=row, column=0, sticky=tk.W, pady=5)
        self.font_var = tk.StringVar()
        self.font_combo = ttk.Combobox(tab1, textvariable=self.font_var, state="readonly", width=40)
        self.font_combo.grid(row=row, column=1, columnspan=3, sticky=tk.W, pady=5, padx=(0,5))
        ttk.Button(tab1, text="刷新字体", command=self.scan_fonts).grid(row=row, column=4, padx=5)

        row += 1
        ttk.Label(tab1, text="字号:").grid(row=row, column=0, sticky=tk.W, pady=5)
        self.font_size_var = tk.IntVar(value=20)
        self.font_size_spinbox = ttk.Spinbox(tab1, from_=5, to=200, textvariable=self.font_size_var, width=8)
        self.font_size_spinbox.grid(row=row, column=1, sticky=tk.W, pady=5)

        row += 1
        ttk.Label(tab1, text="提示:水印文本中使用换行可绘制多行水印", foreground="gray").grid(row=row, column=0, columnspan=5, sticky=tk.W, pady=2)

        # ---------- 标签页2:布局与样式 ----------
        tab2 = ttk.Frame(notebook, padding="10")
        notebook.add(tab2, text="布局与样式")

        row = 0
        ttk.Label(tab2, text="旋转角度:").grid(row=row, column=0, sticky=tk.W, pady=5)
        self.angle_var = tk.IntVar(value=45)
        ttk.Spinbox(tab2, from_=-180, to=180, textvariable=self.angle_var, width=8).grid(row=row, column=1, sticky=tk.W, pady=5)

        ttk.Label(tab2, text="透明度:").grid(row=row, column=2, sticky=tk.W, padx=(20,0), pady=5)
        self.alpha_var = tk.DoubleVar(value=0.25)
        ttk.Spinbox(tab2, from_=0.0, to=1.0, increment=0.05, textvariable=self.alpha_var, width=8).grid(row=row, column=3, sticky=tk.W, pady=5)

        row += 1
        ttk.Label(tab2, text="行间距:").grid(row=row, column=0, sticky=tk.W, pady=5)
        self.spacing_var = tk.IntVar(value=80)
        ttk.Spinbox(tab2, from_=20, to=500, textvariable=self.spacing_var, width=8).grid(row=row, column=1, sticky=tk.W, pady=5)

        ttk.Label(tab2, text="列间距:").grid(row=row, column=2, sticky=tk.W, padx=(20,0), pady=5)
        self.column_var = tk.IntVar(value=500)
        ttk.Spinbox(tab2, from_=100, to=1000, textvariable=self.column_var, width=8).grid(row=row, column=3, sticky=tk.W, pady=5)

        row += 1
        self.use_relative_scale = tk.BooleanVar(value=False)
        ttk.Checkbutton(tab2, text="相对页面比例", variable=self.use_relative_scale).grid(row=row, column=0, sticky=tk.W, pady=5)
        self.relative_scale_var = tk.IntVar(value=50)
        ttk.Spinbox(tab2, from_=1, to=100, textvariable=self.relative_scale_var, width=8).grid(row=row, column=1, sticky=tk.W, pady=5)
        ttk.Label(tab2, text="%").grid(row=row, column=2, sticky=tk.W, pady=5)

        # ---------- 标签页3:位置与范围 ----------
        tab3 = ttk.Frame(notebook, padding="10")
        notebook.add(tab3, text="位置与范围")

        row = 0
        ttk.Label(tab3, text="垂直偏移 (毫米):").grid(row=row, column=0, sticky=tk.W, pady=5)
        self.offset_y_var = tk.DoubleVar(value=0.0)
        ttk.Spinbox(tab3, from_=-100.0, to=100.0, increment=1.0, textvariable=self.offset_y_var, width=8).grid(row=row, column=1, sticky=tk.W, pady=5)

        ttk.Label(tab3, text="水平偏移 (毫米):").grid(row=row, column=2, sticky=tk.W, padx=(20,0), pady=5)
        self.offset_x_var = tk.DoubleVar(value=0.0)
        ttk.Spinbox(tab3, from_=-100.0, to=100.0, increment=1.0, textvariable=self.offset_x_var, width=8).grid(row=row, column=3, sticky=tk.W, pady=5)

        row += 1
        ttk.Label(tab3, text="应用于:").grid(row=row, column=0, sticky=tk.W, pady=5)
        self.page_range_var = tk.StringVar(value="全部页面")
        range_combo = ttk.Combobox(tab3, textvariable=self.page_range_var, values=["全部页面", "仅首页", "自定义"], state="readonly", width=15)
        range_combo.grid(row=row, column=1, sticky=tk.W, pady=5)
        self.custom_range_var = tk.StringVar(value="1-3,5")
        self.custom_range_entry = ttk.Entry(tab3, textvariable=self.custom_range_var, width=20, state=tk.DISABLED)
        self.custom_range_entry.grid(row=row, column=2, columnspan=2, sticky=tk.W, padx=5, pady=5)
        range_combo.bind('<<ComboboxSelected>>', self.on_range_change)

        # 日志与进度条
        log_frame = ttk.LabelFrame(main_frame, text="日志", padding="5")
        log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        self.log_text = scrolledtext.ScrolledText(log_frame, height=6, state=tk.DISABLED)
        self.log_text.pack(fill=tk.BOTH, expand=True)

        self.progress_var = tk.IntVar()
        self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X, pady=5)

        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=5)
        self.start_btn = ttk.Button(btn_frame, text="开始加水印", command=self.start_watermark)
        self.start_btn.pack(side=tk.LEFT, padx=5)
        self.stop_btn = ttk.Button(btn_frame, text="停止", command=self.stop_watermark, state=tk.DISABLED)
        self.stop_btn.pack(side=tk.LEFT, padx=5)

        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)

    def on_mode_change(self):
        if self.input_mode.get() == 'folder':
            for w in self.folder_widgets:
                w.pack(fill=tk.X, pady=2, before=self.output_entry.master)
            for w in self.single_widgets:
                w.pack_forget()
        else:
            for w in self.folder_widgets:
                w.pack_forget()
            for w in self.single_widgets:
                w.pack(fill=tk.X, pady=2, before=self.output_entry.master)

    def on_range_change(self, event=None):
        if self.page_range_var.get() == "自定义":
            self.custom_range_entry.config(state=tk.NORMAL)
        else:
            self.custom_range_entry.config(state=tk.DISABLED)

    def browse_pdf_folder(self):
        path = filedialog.askdirectory(title="选择包含 PDF 的文件夹")
        if path:
            self.pdf_folder_var.set(path)

    def browse_pdf_file(self):
        path = filedialog.askopenfilename(title="选择 PDF 文件", filetypes=[("PDF 文件", "*.pdf")])
        if path:
            self.pdf_file_var.set(path)

    def browse_output_folder(self):
        path = filedialog.askdirectory(title="选择保存水印 PDF 的文件夹")
        if path:
            self.output_folder_var.set(path)

    def scan_fonts(self):
        self.log_message("正在扫描系统字体...")
        self.available_fonts = scan_available_fonts()
        font_names = sorted(self.available_fonts.keys())
        self.font_combo['values'] = font_names
        preferred = ['楷体', 'Kaiti SC', 'KaiTi', '宋体', 'Songti SC', 'SimSun', '微软雅黑', 'PingFang SC']
        for name in preferred:
            if name in font_names:
                self.font_var.set(name)
                break
        else:
            if font_names:
                self.font_var.set(font_names[0])
        preview = ', '.join(font_names[:5]) + ('...' if len(font_names) > 5 else '')
        self.log_message(f"字体扫描完成,共找到 {len(font_names)} 种字体:{preview}")

    def log_message(self, msg):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.insert(tk.END, msg + "\n")
        self.log_text.see(tk.END)
        self.log_text.config(state=tk.DISABLED)
        self.status_var.set(msg)

    def start_watermark(self):
        input_mode = self.input_mode.get()
        if input_mode == 'folder':
            if not self.pdf_folder_var.get():
                messagebox.showwarning("警告", "请选择 PDF 文件夹")
                return
        else:
            if not self.pdf_file_var.get():
                messagebox.showwarning("警告", "请选择 PDF 文件")
                return

        output_folder = self.output_folder_var.get().strip()
        if not self.font_var.get():
            messagebox.showwarning("警告", "请选择一种字体")
            return
        font_path = self.available_fonts.get(self.font_var.get())
        if font_path is None:
            messagebox.showwarning("警告", "所选字体路径无效")
            return

        watermark_text = self.watermark_entry.get("1.0", tk.END).strip()
        if not watermark_text:
            messagebox.showwarning("警告", "请输入水印文字")
            return

        range_type = self.page_range_var.get()
        page_range_str = "全部页面" if range_type == "全部页面" else ("1" if range_type == "仅首页" else self.custom_range_var.get().strip())

        settings = {
            'input_mode': input_mode,
            'pdf_folder': self.pdf_folder_var.get() if input_mode == 'folder' else '',
            'pdf_file': self.pdf_file_var.get() if input_mode == 'single' else '',
            'output_folder': output_folder,
            'watermark_text': watermark_text,
            'font_family': self.font_var.get(),
            'font_path': font_path,
            'font_size': self.font_size_var.get(),
            'use_relative_scale': self.use_relative_scale.get(),
            'relative_scale_percent': self.relative_scale_var.get(),
            'angle': self.angle_var.get(),
            'alpha': self.alpha_var.get(),
            'row_spacing': self.spacing_var.get(),
            'col_spacing': self.column_var.get(),
            'offset_x_mm': self.offset_x_var.get(),
            'offset_y_mm': self.offset_y_var.get(),
            'page_range': page_range_str,
        }

        self.start_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)
        self.progress_var.set(0)
        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete(1.0, tk.END)
        self.log_text.config(state=tk.DISABLED)

        self.watermark_thread = WatermarkThread(settings, self.progress_queue)
        self.watermark_thread.start()

    def stop_watermark(self):
        if self.watermark_thread and self.watermark_thread.is_alive():
            self.watermark_thread.stop_process()
            self.log_message("正在停止...")
        else:
            self.reset_ui()

    def reset_ui(self):
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)

    def check_queue(self):
        try:
            while True:
                progress, msg = self.progress_queue.get_nowait()
                self.progress_var.set(progress)
                self.log_message(msg)
                if progress == 100 or "所有任务完成" in msg or "用户中止" in msg:
                    self.reset_ui()
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self.check_queue)


if __name__ == '__main__':
    root = tk.Tk()
    app = PDFWatermarkApp(root)
    root.mainloop()


六、成品链接

文件界面截图

文件界面截图

百度网盘:https://pan.baidu.com/s/13EIu-sT2BJ9zSuFcZdAQHw?pwd=rz3h 提取码: rz3h

七、最后说一句

感谢原版作者 `pythonfun` 的分享,没有他的基础代码我也懒得从头写。我这个版本纯粹是自己用着顺手顺手改的,如果大家也有类似需求,可以拿去试试。有问题也可以回帖或者私信,我看到会回。

免费评分

参与人数 7吾爱币 +13 热心值 +7 收起 理由
ablove37 + 1 + 1 非常好用,谢谢
shengli123 + 1 + 1 我很赞同!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
miocaro507 + 1 + 1 我很赞同!
vivvon + 1 + 1 我很赞同!
bxf2080 + 1 + 1 谢谢@Thanks!
yun33 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| 人间值得 发表于 2026-4-16 17:01

增加图片水印功能

本帖最后由 人间值得 于 2026-8-19 09:47 编辑

通过网盘分享的文件:PDF水印工具V1.5.exe
链接: https://pan.baidu.com/s/1wjgehMYMKx4trlBASlQFyw 提取码: psqu
--来自百度网盘超级会员v9的分享
 楼主| 人间值得 发表于 2026-4-23 18:47
v1.2.0 (2026-04-23) —— 稳定性修复与文件夹模式问题解决
&#128027; Bug 修复

修复文件夹批量处理完全失效的问题
根源在于多个 PDF 共用同一个临时水印文件 __temp_watermark.pdf,导致文件覆盖或残留冲突,现已为每个 PDF 生成唯一的临时文件(UUID 命名)。

修复字体重复注册导致的崩溃
在文件夹模式下连续处理文件时,reportlab 会因重复注册字体抛出异常,现已添加异常捕获,确保字体仅注册一次,不会中断流程。

修复输出目录不可写时无提前告警
某些系统目录可能无写入权限,程序现在会在处理前验证输出目录的可写性,避免处理到一半失败。

修复日志中无法看到详细处理步骤的问题
增加了“创建水印层”、“合并 PDF”、“清理临时文件”等关键步骤的日志输出,便于定位具体失败位置。

&#10024; 改进优化

增强了日志系统的可读性,进度提示更清晰。

减少了临时文件残留的概率(即使删除失败也不会影响其他文件)。

优化了字体注册逻辑,提升了处理首次运行时的稳定性。

百度网盘链接: https://pan.baidu.com/s/1gY2bw0oJ31wrn2QmYrJL0A?pwd=jvmn 提取码: jvmn
 楼主| 人间值得 发表于 2026-4-28 11:32
jacktong85 发表于 2026-4-27 10:57
Win10  22H2系统,64位。对,不能加载字体

已在我搭建的Win10  22H2 64位系统测试通过,可以用这个版本看看
通过网盘分享的文件:PDF水印工具V1.3.exe
链接: https://pan.baidu.com/s/1UWeH_CrEKK-H98kdZtvHkw?pwd=m65q 提取码: m65q
 楼主| 人间值得 发表于 2026-8-19 09:46
人间值得 发表于 2026-4-16 17:01
通过网盘分享的文件:PDF水印工具V1.1.exe
链接: https://pan.baidu.com/s/1Yc7NVpIzyOESSd4XZZDDxw?pwd=n ...

v1.5.0(2026-08-19) —— 增加图片水印功能
通过网盘分享的文件:PDF水印工具V1.5.exe
链接: https://pan.baidu.com/s/1H7JPXlt8M7pznIXobgApug 提取码: gyen
--来自百度网盘超级会员v9的分享
 楼主| 人间值得 发表于 2026-8-19 09:48
ablove37 发表于 2026-8-15 10:45
感谢楼主!非常好用。论坛找了一圈,楼主的这款给pdf增加水印的工具最好用最完美,但是给图片增加水印的工 ...

增加图片水印功能
本帖最后由 人间值得 于 2026-8-19 09:47 编辑


通过网盘分享的文件:PDF水印工具V1.5.exe
链接: https://pan.baidu.com/s/1wjgehMYMKx4trlBASlQFyw 提取码: psqu
--来自百度网盘超级会员v9的分享
q1wv 发表于 2026-4-16 09:05
感谢 学会在PDF里画图了
你的Tkinter界面漂亮 一眼就能看懂
lindsay12 发表于 2026-4-16 09:11
能够批量操作还是挺方便的,谢谢楼主分享
lyliucn 发表于 2026-4-15 15:52
有没去水印工具。
Mzhang2008 发表于 2026-4-15 15:54
批量添加很方便
jacktong85 发表于 2026-4-15 17:05
试了下,这个批量的挺好用的
hk137 发表于 2026-4-15 17:26
试一试看看
woawapj 发表于 2026-4-15 17:30
这个工具看看了
cars888 发表于 2026-4-15 18:25
看上去是真不错,下一个看看吧
zhu791023529 发表于 2026-4-15 23:43
psName=b'\xe6\x8e\xa2\xe7\xb4\xa2\xe8\x80\x85\xe4\xbb\xbf\xe5\xae\x8b' contains invalid character 230,添加水印时,出现这个错误,什么意思?
mnp818 发表于 2026-4-16 05:40
收藏备用,实际上去水印的使用场景更多
miocaro507 发表于 2026-4-16 07:57
添加的都是贴心的功能
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-13 10:26

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表