《PDF暗水印实战指南:从隐蔽添加到逆向解析,三步实现文档追踪与版权保护》

一、引言

在数字化信息时代,版权保护、内容追踪和数据安全成为重要课题。暗水印技术作为一种隐蔽的数字信息嵌入方法,能在不影响原始内容视觉效果的前提下,将特定标识或信息嵌入文件中,为数字内容的版权归属和使用追踪提供有效手段。本文将详细介绍暗水印技术的原理,并通过Python结合Flask框架,实现对PDF文件和图片的暗水印添加与提取功能。

二、暗水印简介

暗水印(Invisible Watermark),又称不可见水印,是一种将特定信息(如版权声明、用户标识等)嵌入到数字内容(如图像、音频、视频、文档)中的技术。与可见水印不同,暗水印在正常查看时不可见,需通过特定算法或工具才能提取,既保护了内容的完整性和美观性,又能在必要时验证内容来源或追踪使用情况。

三、暗水印技术原理

暗水印技术的核心是信息隐藏,其原理基于数字信号处理和人类感知系统的局限性。以图像水印为例,常见的实现方式是利用图像像素值的冗余空间,通过修改像素的最低有效位(Least Significant Bit, LSB)或调整频率域系数来嵌入水印信息。

1. 空间域方法(以LSB为例)

  • 嵌入过程:将水印信息编码为二进制数据,替换图像像素值的最低几位(如最低1位或2位)。由于人类视觉系统对像素值的微小变化不敏感,这种修改不会影响图像的视觉效果。
  • 提取过程:从嵌入水印的图像中提取对应像素位,解码后恢复水印信息。

2. 频率域方法

  • 嵌入过程:利用傅里叶变换或离散余弦变换(DCT)将图像从空间域转换到频率域,在高频或中频系数中嵌入水印信息。高频系数的微小变化对图像质量影响较小。
  • 提取过程:对含水印图像进行逆变换,从频率域系数中提取水印信息。

四、基于Python的暗水印实现方式

1. 环境与依赖

本实现基于Python,依赖以下库:

  • Flask:用于搭建API接口。
  • PyMuPDF(fitz):处理PDF文件。
  • OpenCV(cv2):图像处理。
  • Pillow(PIL):图像绘制与转换。
  • requests:用于从URL下载文件。

可通过以下命令安装:

pip install flask pymupdf opencv-python pillow requests

2. 关键代码实现

(1)添加暗水印到PDF文件
import fitz
import cv2
import numpy as np
from PIL import Image ,ImageDraw, ImageFont
import io


def add_watermark_to_pdf(input_pdf_path, output_pdf_path, watermark_text):
    try:
        doc = fitz.open(input_pdf_path)
        for page in doc:
            # 获取当前页面的像素图
            pix = page.get_pixmap()
            # 将像素图转换为 PIL 图像
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
            # 将 PIL 图像转换为 NumPy 数组
            img_np = np.array(img)
            # 转换颜色空间为 BGR
            img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
            # 生成水印图像
            watermark_image = generate_watermark_image(img_np.shape, watermark_text)
            if watermark_image is None:
                print("生成水印图像失败,跳过当前页面")
                continue
            # 将水印嵌入到图像中
            watermarked_image = embed_watermark(img_np, watermark_image)
            # 转换颜色空间回 RGB
            watermarked_image = cv2.cvtColor(watermarked_image, cv2.COLOR_BGR2RGB)
            # 将 NumPy 数组转换为 PIL 图像
            watermarked_pil = Image.fromarray(watermarked_image)
            # 创建一个内存中的字节流
            img_byte_arr = io.BytesIO()
            # 将 PIL 图像保存为 PNG 格式到字节流
            watermarked_pil.save(img_byte_arr, format='PNG')
            img_byte_arr = img_byte_arr.getvalue()
            # 将处理后的图像插入到当前页面
            page.insert_image(page.rect, stream=img_byte_arr)
        # 保存修改后的 PDF 文件
        doc.save(output_pdf_path)
        # 关闭 PDF 文件
        doc.close()
    except Exception as e:
        print(f"添加水印时出错: {e}")

def generate_watermark_image(shape, watermark_text):
    try:
        watermark_image = np.zeros(shape[:2], dtype=np.uint8)
        pil_img = Image.fromarray(watermark_image)
        draw = ImageDraw.Draw(pil_img)
        font = ImageFont.truetype("simhei.ttf", 40)
        width, height = shape[1], shape[0]
        # 计算文本的宽度和高度
        left, top, right, bottom = font.getbbox(watermark_text)
        text_width = right - left
        text_height = bottom - top

        # 固定倾斜角度
        angle = 30
        # 计算旋转后的文本宽度和高度
        line_img = Image.new('L', (text_width, text_height))
        line_draw = ImageDraw.Draw(line_img)
        line_draw.text((0, 0), watermark_text, font=font, fill=255)
        rotated_line = line_img.rotate(angle, expand=True)
        rotated_width, rotated_height = rotated_line.size

        # 计算水平和垂直间距
        x_interval = rotated_width + 20  # 增加20像素的间隔
        y_interval = rotated_height + 20

        for y in range(0, height, y_interval):
            for x in range(0, width, x_interval):
                paste_x = x
                paste_y = y
                pil_img.paste(rotated_line, (paste_x, paste_y), rotated_line)
        watermark_image = np.array(pil_img)
        return watermark_image
    except OSError:
        print("未找到 simhei.ttf 字体文件,请检查。")
        return None


def embed_watermark(image, watermark):
    # 调整水印图像的大小与原图像一致
    watermark = cv2.resize(watermark, (image.shape[1], image.shape[0]))
    # 对水印图像进行二值化处理
    _, watermark = cv2.threshold(watermark, 127, 255, cv2.THRESH_BINARY)
    # 将图像数据类型转换为 int16 以避免溢出问题
    image = image.astype(np.int16)
    watermark = watermark.astype(np.int16)
    # 复制原图像
    watermarked_image = image.copy()
    # 将水印信息嵌入到图像的最低有效位
    watermarked_image[:, :, 0] = (watermarked_image[:, :, 0] & 0xFE) | (watermark >> 7)
    # 确保像素值在 0 到 255 之间
    watermarked_image = np.clip(watermarked_image, 0, 255).astype(np.uint8)
    return watermarked_image


def extract_watermark_from_image(image):
    watermark = (image[:, :, 0] & 1) * 255
    return watermark
(2)从pdf文件中提取暗水印
def extract_watermark_from_pdf(pdf_path):
    doc = fitz.open(pdf_path)
    for page in doc:
        pix = page.get_pixmap()
        img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
        img_np = np.array(img)
        img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
        watermark = extract_watermark_from_image(img_np)
        _, binary_watermark = cv2.threshold(watermark, 127, 255, cv2.THRESH_BINARY)
        contours, _ = cv2.findContours(binary_watermark, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if contours:
            doc.close()
            return binary_watermark
    doc.close()
    return None
(3)从图片中提取暗水印
def extract_watermark_from_image2(image_path):
    try:
        img = cv2.imread(image_path)
        if img is None:
            print("无法读取图片,请检查图片路径。")
            return None
        watermark = (img[:, :, 0] & 1) * 255
        _, binary_watermark = cv2.threshold(watermark, 127, 255, cv2.THRESH_BINARY)
        contours, _ = cv2.findContours(binary_watermark, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if contours:
            return binary_watermark
        return None
    except Exception as e:
        print(f"提取水印时出错: {e}")
        return None

3. 完整版接口封装

# coding: gbk
import requests
import fitz
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import io
from flask import Flask, request, jsonify
from rapidocr_pdf import PDFExtracter
from flask import Blueprint
import os
from flask import send_file, make_response
import random

pdfWatermark = Blueprint('pdfWatermark', __name__)
# 设置上传文件存放的目录
UPLOAD_FOLDER = './pdfWatermark'
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

# 允许的文件扩展名
ALLOWED_EXTENSIONS = {'pdf'}
# 允许的文件扩展名
IMAGE_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
# 初始化PDFExtracter
pdf_extracter = PDFExtracter()


def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


def allowed_image(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in IMAGE_EXTENSIONS


@pdfWatermark.route('/api/add_watermark_to_pdf', methods=['POST'])
def add_watermark_to_pdf():
    try:
        # 检查请求中是否包含文件
        if 'file' not in request.files:
            return jsonify({"success": "false", "error": "请求中没有文件"}), 400
        file = request.files['file']

        if file.filename == '':
            return jsonify({"success": "false", "error": "没有找到文件"}), 400

        if file and allowed_file(file.filename):
            # 获取水印文本
            watermark_text = request.form.get('watermark_text')
            if not watermark_text:
                return jsonify({"success": "false", "error": "未提供水印文本"}), 400
            #获取倾斜角度
            angle =  request.form.get('angle')
            if not angle:
                return jsonify({"success": "false", "error": "未提供倾斜角度"}), 400
            angle = int(angle)
            #获取字体大小
            font_size =  request.form.get('font_size')
            if not font_size:
                return jsonify({"success": "false", "error": "未提供水字体大小"}), 400
            font_size = int(font_size)
            # 保存上传的文件
            six_digit = ''.join(random.choices('0123456789', k=6))
            pdf_path = os.path.join(UPLOAD_FOLDER, six_digit + file.filename)
            file.save(pdf_path)

            # 处理 PDF 文件
            doc = fitz.open(pdf_path)
            for page in doc:
                pix = page.get_pixmap()
                img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
                img_np = np.array(img)
                img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
                watermark_image = generate_watermark_image(img_np.shape, watermark_text,angle,font_size)
                if watermark_image is None:
                    print("生成水印图像失败,跳过当前页面")
                    continue
                watermarked_image = embed_watermark(img_np, watermark_image)
                watermarked_image = cv2.cvtColor(watermarked_image, cv2.COLOR_BGR2RGB)
                watermarked_pil = Image.fromarray(watermarked_image)
                img_byte_arr = io.BytesIO()
                watermarked_pil.save(img_byte_arr, format='PNG')
                img_byte_arr = img_byte_arr.getvalue()
                page.insert_image(page.rect, stream=img_byte_arr)
            # 保存修改后的 PDF 文件到内存
            output_pdf_path = os.path.join(UPLOAD_FOLDER, 'watermarked_' + six_digit + file.filename)
            doc.save(output_pdf_path)
            doc.close()
            # 读取处理后的 PDF 文件并返回响应
            with open(output_pdf_path, 'rb') as f:
                pdf_bytes = f.read()
            response = make_response(pdf_bytes)
            response.headers.set('Content-Type', 'application/pdf')
            response.headers.set(
                'Content-Disposition', 'attachment', filename='watermarked_' + file.filename
            )
            # 删除临时文件
            if os.path.exists(pdf_path):
                os.remove(pdf_path)
            if os.path.exists(output_pdf_path):
                os.remove(output_pdf_path)
            return response
        else:
            return jsonify({"success": "false", "error": "不允许的文件类型"}), 400

    except Exception as e:
        print(f"添加水印时出错: {e}")
        return jsonify({"success": "false", "error": str(e)}), 500


@pdfWatermark.route('/api/add_watermark_to_url', methods=['POST'])
def add_watermark_to_url():
    url = request.form.get('url')  # 获取请求中的 URL
    if not url:
        return jsonify({"success": "false", "error": "请求中没有提供 URL"}), 400
    try:
        # 下载文件
        response = requests.get(url)
        if response.status_code != 200:
            return jsonify({"success": "false", "error": "无法下载文件"}), 400
        # 确定文件名,这里简单地使用了最后一个斜杠后的字符串
        file_name = url.split('/')[-1]
        if not allowed_file(file_name):
            return jsonify({"success": "false", "error": "不允许的文件类型"}), 400
        # 获取水印文本
        watermark_text = request.form.get('watermark_text')
        if not watermark_text:
            return jsonify({"success": "false", "error": "未提供水印文本"}), 400
        # 获取倾斜角度
        angle = request.form.get('angle')
        if not angle:
            return jsonify({"success": "false", "error": "未提供倾斜角度"}), 400
        angle  = int(angle)
        # 获取字体大小
        font_size =  request.form.get('font_size')
        if not font_size:
            return jsonify({"success": "false", "error": "未提供水字体大小"}), 400
        font_size = int(font_size)
        # 保存下载的文件
        six_digit = ''.join(random.choices('0123456789', k=6))
        pdf_path = os.path.join(UPLOAD_FOLDER, six_digit + file_name)
        with open(pdf_path, 'wb') as f:
            f.write(response.content)

        # 处理 PDF 文件
        doc = fitz.open(pdf_path)
        for page in doc:
            pix = page.get_pixmap()
            img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
            img_np = np.array(img)
            img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
            watermark_image = generate_watermark_image(img_np.shape, watermark_text,angle,font_size)
            if watermark_image is None:
                print("生成水印图像失败,跳过当前页面")
                continue
            watermarked_image = embed_watermark(img_np, watermark_image)
            watermarked_image = cv2.cvtColor(watermarked_image, cv2.COLOR_BGR2RGB)
            watermarked_pil = Image.fromarray(watermarked_image)
            img_byte_arr = io.BytesIO()
            watermarked_pil.save(img_byte_arr, format='PNG')
            img_byte_arr = img_byte_arr.getvalue()
            page.insert_image(page.rect, stream=img_byte_arr)
        # 保存修改后的 PDF 文件到内存
        output_pdf_path = os.path.join(UPLOAD_FOLDER, 'watermarked_' + six_digit + file_name)
        doc.save(output_pdf_path)
        doc.close()
        # 读取处理后的 PDF 文件并返回响应
        with open(output_pdf_path, 'rb') as f:
            pdf_bytes = f.read()
        response = make_response(pdf_bytes)
        response.headers.set('Content-Type', 'application/pdf')
        response.headers.set(
            'Content-Disposition', 'attachment', filename='watermarked_' + file_name
        )
        # 删除临时文件
        if os.path.exists(pdf_path):
            os.remove(pdf_path)
        if os.path.exists(output_pdf_path):
            os.remove(output_pdf_path)
        return response
    except requests.RequestException as e:
        print(f"下载文件时出错: {e}")
        return jsonify({"success": "false", "error": f"下载文件时出错: {e}"}), 500
    except Exception as e:
        print(f"添加水印时出错: {e}")
        return jsonify({"success": "false", "error": f"添加水印时出错: {e}"}), 500


def generate_watermark_image(shape, watermark_text,angle,font_size):
    try:
        watermark_image = np.zeros(shape[:2], dtype=np.uint8)
        pil_img = Image.fromarray(watermark_image)
        draw = ImageDraw.Draw(pil_img)
        font = ImageFont.truetype("simhei.ttf", font_size)
        width, height = shape[1], shape[0]
        # 计算文本的宽度和高度
        left, top, right, bottom = font.getbbox(watermark_text)
        text_width = right - left
        text_height = bottom - top
        # 计算旋转后的文本宽度和高度
        line_img = Image.new('L', (text_width, text_height))
        line_draw = ImageDraw.Draw(line_img)
        line_draw.text((0, 0), watermark_text, font=font, fill=255)
        rotated_line = line_img.rotate(angle, expand=True)
        rotated_width, rotated_height = rotated_line.size

        # 计算水平和垂直间距
        x_interval = rotated_width + 20  # 增加20像素的间隔
        y_interval = rotated_height + 20

        for y in range(0, height, y_interval):
            for x in range(0, width, x_interval):
                paste_x = x
                paste_y = y
                pil_img.paste(rotated_line, (paste_x, paste_y), rotated_line)
        watermark_image = np.array(pil_img)
        return watermark_image
    except OSError:
        print("未找到 simhei.ttf 字体文件,请检查。")
        return None


def embed_watermark(image, watermark):
    # 调整水印图像的大小与原图像一致
    watermark = cv2.resize(watermark, (image.shape[1], image.shape[0]))
    # 对水印图像进行二值化处理
    _, watermark = cv2.threshold(watermark, 127, 255, cv2.THRESH_BINARY)
    # 将图像数据类型转换为 int16 以避免溢出问题
    image = image.astype(np.int16)
    watermark = watermark.astype(np.int16)
    # 复制原图像
    watermarked_image = image.copy()
    # 将水印信息嵌入到图像的最低有效位
    watermarked_image[:, :, 0] = (watermarked_image[:, :, 0] & 0xFE) | (watermark >> 7)
    # 确保像素值在 0 到 255 之间
    watermarked_image = np.clip(watermarked_image, 0, 255).astype(np.uint8)
    return watermarked_image


def extract_watermark_image(image):
    watermark = (image[:, :, 0] & 1) * 255
    return watermark


@pdfWatermark.route('/api/extract_watermark_from_pdf', methods=['POST'])
def extract_watermark_from_pdf():
    try:
        # 检查请求中是否包含文件
        if 'file' not in request.files:
            return jsonify({"success": "false", "error": "请求中没有文件"}), 400
        file = request.files['file']

        if file.filename == '':
            return jsonify({"success": "false", "error": "没有找到文件"}), 400

        if file and allowed_file(file.filename):
            # 保存上传的文件
            six_digit = ''.join(random.choices('0123456789', k=6))
            file_path = os.path.join(UPLOAD_FOLDER, six_digit + file.filename)
            file.save(file_path)
            doc = fitz.open(file_path)
            for page in doc:
                pix = page.get_pixmap()
                img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
                img_np = np.array(img)
                img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
                watermark = extract_watermark_image(img_np)
                _, binary_watermark = cv2.threshold(watermark, 127, 255, cv2.THRESH_BINARY)
                contours, _ = cv2.findContours(binary_watermark, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
                if contours:
                    # 将 NumPy 数组转换为 PIL 图像
                    watermark_img = Image.fromarray(binary_watermark)
                    # 创建一个内存中的字节流
                    img_byte_arr = io.BytesIO()
                    # 将 PIL 图像保存为 PNG 格式到字节流
                    watermark_img.save(img_byte_arr, format='PNG')
                    img_byte_arr.seek(0)
                    # 返回响应
                    response = make_response(img_byte_arr.getvalue())
                    response.headers.set('Content-Type', 'image/png')
                    response.headers.set(
                        'Content-Disposition', 'attachment', filename='watermarked_' + six_digit + ".png"
                    )
                    # 删除临时文件
                    doc.close()
                    if os.path.exists(file_path):
                        os.remove(file_path)
                    return response

            # 所有页面都未提取到水印
            doc.close()
            if os.path.exists(file_path):
                os.remove(file_path)
            return jsonify({"success": "false", "error": "未提取到暗水印"}), 400
        else:
            return jsonify({"success": "false", "error": "不允许的文件类型"}), 400
    except Exception as e:
        print(f"提取水印时出错: {e}")
        return jsonify({"success": "false", "error": str(e)}), 500


# 根据图片提取暗水印
@pdfWatermark.route('/api/extract_watermark_from_image', methods=['POST'])
def extract_watermark_from_image():
    try:
        # 检查请求中是否包含文件
        if 'file' not in request.files:
            return jsonify({"success": "false", "error": "请求中没有文件"}), 400
        file = request.files['file']

        if file.filename == '':
            return jsonify({"success": "false", "error": "没有找到文件"}), 400

        if file and allowed_image(file.filename):
            # 保存上传的文件
            six_digit = ''.join(random.choices('0123456789', k=6))
            image_path = os.path.join(UPLOAD_FOLDER, six_digit + file.filename)
            file.save(image_path)

            img = cv2.imread(image_path)
            if img is None:
                return jsonify({"success": "false", "error": "无法读取图片,请检查图片路径。"}), 400

            watermark = (img[:, :, 0] & 1) * 255
            _, binary_watermark = cv2.threshold(watermark, 127, 255, cv2.THRESH_BINARY)
            contours, _ = cv2.findContours(binary_watermark, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

            if contours:
                # 将 NumPy 数组转换为 PIL 图像
                watermark_img = Image.fromarray(binary_watermark)
                # 创建一个内存中的字节流
                img_byte_arr = io.BytesIO()
                # 将 PIL 图像保存为 PNG 格式到字节流
                watermark_img.save(img_byte_arr, format='PNG')
                img_byte_arr.seek(0)

                # 返回响应
                response = make_response(img_byte_arr.getvalue())
                response.headers.set('Content-Type', 'image/png')
                response.headers.set(
                    'Content-Disposition', 'attachment', filename='watermarked_' + file.filename
                )

                # 删除临时文件
                if os.path.exists(image_path):
                    os.remove(image_path)

                return response

            # 删除临时文件
            if os.path.exists(image_path):
                os.remove(image_path)

            return jsonify({"success": "false", "error": "未提取到暗水印"}), 400
        else:
            return jsonify({"success": "false", "error": "不允许的图片类型"}), 400
    except Exception as e:
        print(f"提取水印时出错: {e}")
        return jsonify({"success": "false", "error": str(e)}), 500

4. 运行效果

(1)原文

在这里插入图片描述

(1)生成的暗水印文件

在这里插入图片描述

(3)pdf文件解析后的效果

在这里插入图片描述

(3)截图图片解析后的效果

在这里插入图片描述

5.结果总结

结果对比文件大小清晰度
原文500K高清
处理后文件15M不高清

原因:因为采用的是图像合成方式,将水印图像隐藏到了原本文件中的每一页,所以文件大小会变的指数增大。

五、暗水印技术的优势与缺点

1. 优势

  • 隐蔽性强:不影响原始内容的视觉或听觉效果,难以被察觉和篡改。
  • 版权保护:明确内容归属,在版权纠纷中提供有力证据。
  • 追踪溯源:嵌入用户标识或时间戳,追踪内容传播路径。
  • 多功能性:适用于多种数字媒体类型,如图片、视频、文档等。

2. 缺点

  • 鲁棒性挑战:易受压缩、裁剪、滤波等处理影响,导致水印丢失或损坏。
  • 误判风险:提取算法可能因噪声或相似数据产生误判,需精确设计算法。
  • 技术门槛:对嵌入和提取算法要求较高,需专业知识支持。
  • 法律争议:不同地区对水印技术的法律认定存在差异,可能引发争议。

六、总结

暗水印技术作为数字内容保护的重要手段,通过隐蔽的信息嵌入实现版权追踪与内容防伪。本文通过Python和Flask框架,展示了暗水印在PDF和图片中的具体实现方式,涵盖从理论原理到代码实践的完整流程。尽管暗水印存在鲁棒性和法律等方面的挑战,但随着技术的发展,其在数字版权管理、数据安全等领域的应用前景依然广阔。

通过进一步优化算法(如采用深度学习增强鲁棒性)和结合区块链技术,暗水印有望为数字内容生态提供更可靠的安全保障。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

游戏人生的NPC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值