文章目录
一、引言
在数字化信息时代,版权保护、内容追踪和数据安全成为重要课题。暗水印技术作为一种隐蔽的数字信息嵌入方法,能在不影响原始内容视觉效果的前提下,将特定标识或信息嵌入文件中,为数字内容的版权归属和使用追踪提供有效手段。本文将详细介绍暗水印技术的原理,并通过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和图片中的具体实现方式,涵盖从理论原理到代码实践的完整流程。尽管暗水印存在鲁棒性和法律等方面的挑战,但随着技术的发展,其在数字版权管理、数据安全等领域的应用前景依然广阔。
通过进一步优化算法(如采用深度学习增强鲁棒性)和结合区块链技术,暗水印有望为数字内容生态提供更可靠的安全保障。