该方案在Xcode构建过程中对App Bundle内的PNG图片资源进行最低有效位(LSB)嵌入,将水印信息隐藏于像素数据中,不影响视觉效果且可追踪版权,当然其他的作用,自己进行体会。
一、方案核心
- 水印算法:LSB(最低有效位)隐写术,将水印二进制串嵌入图片每个像素RGB分量的最低位。人眼无法察觉,满足"隐式增加"。
- 处理时机 :在
Copy Bundle Resources阶段之后,对已拷贝到应用包中的图片进行原地修改,不污染源文件。 - 开关控制 :通过Xcode Build Settings中的自定义变量
ENABLE_INVISIBLE_WATERMARK控制是否执行,默认关闭。 - 适用格式 :PNG(无损压缩,LSB稳定),可扩展支持BMP。不处理JPEG(有损压缩会破坏水印)。
二、实现代码
2.1 Python脚本 invisible_watermark.py
将此脚本放入项目根目录的Scripts文件夹中。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import struct
from PIL import Image
# ================== 配置 ==================
# 水印内容(建议使用项目标识+构建时间,保证唯一性)
WATERMARK_TEXT = "COPYRIGHT_YOURAPP_2026"
# 水印起始标记(用于定位,避免误读)
START_MARKER = 0xAA # 10101010
END_MARKER = 0x55 # 01010101
# ==========================================
def text_to_bits(text):
"""将字符串转为二进制位列表(含起始/结束标记)"""
# 添加起始标记(8位)
bits = [int(b) for b in format(START_MARKER, '08b')]
# 添加文本长度(32位,大端)
text_bytes = text.encode('utf-8')
length_bits = []
for byte in struct.pack('>I', len(text_bytes)):
length_bits.extend([int(b) for b in format(byte, '08b')])
bits.extend(length_bits)
# 添加文本内容
for byte in text_bytes:
bits.extend([int(b) for b in format(byte, '08b')])
# 添加结束标记(8位)
bits.extend([int(b) for b in format(END_MARKER, '08b')])
return bits
def embed_lsb(image_path, bits):
"""将二进制位嵌入图片的RGB最低有效位,返回是否成功"""
img = Image.open(image_path).convert('RGB')
pixels = img.load()
width, height = img.size
total_bits = width * height * 3 # 每个像素3个通道
if len(bits) > total_bits:
print(f" 错误: 图片容量不足,需要{len(bits)}位,实际{total_bits}位")
return False
idx = 0
for y in range(height):
for x in range(width):
r, g, b = pixels[x, y]
if idx < len(bits):
r = (r & 0xFE) | bits[idx]
idx += 1
if idx < len(bits):
g = (g & 0xFE) | bits[idx]
idx += 1
if idx < len(bits):
b = (b & 0xFE) | bits[idx]
idx += 1
pixels[x, y] = (r, g, b)
if idx >= len(bits):
break
if idx >= len(bits):
break
img.save(image_path, format='PNG', optimize=False)
return True
def process_bundle(bundle_path):
"""遍历App Bundle中的PNG图片,嵌入水印"""
processed = 0
for root, dirs, files in os.walk(bundle_path):
for file in files:
if file.lower().endswith('.png'):
file_path = os.path.join(root, file)
try:
bits = text_to_bits(WATERMARK_TEXT)
if embed_lsb(file_path, bits):
print(f" ✓ 已处理: {os.path.relpath(file_path, bundle_path)}")
processed += 1
else:
print(f" ✗ 容量不足,跳过: {file_path}")
except Exception as e:
print(f" ✗ 处理失败 {file_path}: {e}")
return processed
def main():
if len(sys.argv) < 2:
print("用法: python3 invisible_watermark.py <App Bundle路径>")
sys.exit(1)
bundle_path = sys.argv[1]
if not os.path.isdir(bundle_path):
print(f"错误: 路径不存在或不是目录: {bundle_path}")
sys.exit(1)
print(f"开始处理Bundle: {bundle_path}")
print(f"水印内容: {WATERMARK_TEXT}")
count = process_bundle(bundle_path)
print(f"完成,共处理 {count} 个PNG图片")
if __name__ == '__main__':
main()
2.2 Xcode Run Script 集成
在Xcode项目Target的Build Phases中添加一个新的Run Script Phase,放在Copy Bundle Resources之后,确保资源已拷贝到App Bundle。
脚本内容:
bash
# 不可见水印开关:仅在配置了ENABLE_INVISIBLE_WATERMARK且值为YES时执行
if [ "${ENABLE_INVISIBLE_WATERMARK}" != "YES" ]; then
echo "🔇 不可见水印已禁用 (ENABLE_INVISIBLE_WATERMARK != YES)"
exit 0
fi
# 检查Python3环境
if ! command -v python3 &> /dev/null; then
echo "⚠️ 未找到python3,跳过水印处理"
exit 0
fi
# 检查Pillow库,若未安装则自动安装(可选)
python3 -c "import PIL" 2>/dev/null
if [ $? -ne 0 ]; then
echo "📦 安装Pillow库..."
pip3 install Pillow --user --quiet
fi
# 获取App Bundle路径(针对模拟器和真机统一处理)
APP_PATH="${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}"
if [ ! -d "$APP_PATH" ]; then
echo "❌ 未找到App Bundle: $APP_PATH"
exit 1
fi
# 脚本所在路径(假设放在项目根目录/Scripts/下)
SCRIPT_PATH="${SRCROOT}/Scripts/invisible_watermark.py"
if [ ! -f "$SCRIPT_PATH" ]; then
echo "❌ 未找到水印脚本: $SCRIPT_PATH"
exit 1
fi
echo "🔏 开始嵌入不可见水印..."
python3 "$SCRIPT_PATH" "$APP_PATH"
echo "✅ 水印嵌入完成"
2.3 开关配置方式
在Xcode项目的Build Settings中添加User-Defined Setting:
- 选择Target → Build Settings
- 点击
+→Add User-Defined Setting - 设置Key为
ENABLE_INVISIBLE_WATERMARK - 设置Value为
YES(启用)或NO(禁用)
建议不同配置使用不同值:
- Debug:
NO(加快构建) - Release:
YES(正式包加水印)
通过Build Configuration下的ENABLE_INVISIBLE_WATERMARK分别设置即可。
三、验证水印存在性(可选,用于追踪)
如果需要从图片中提取水印以验证版权,可提供以下提取脚本(单独使用,不在构建时执行):
python
#!/usr/bin/env python3
import sys
from PIL import Image
def extract_lsb(image_path):
img = Image.open(image_path).convert('RGB')
pixels = img.load()
width, height = img.size
bits = []
for y in range(height):
for x in range(width):
r, g, b = pixels[x, y]
bits.append(r & 1)
bits.append(g & 1)
bits.append(b & 1)
# 查找起始标记
start_marker_bits = [int(b) for b in format(0xAA, '08b')]
for i in range(len(bits) - 8):
if bits[i:i+8] == start_marker_bits:
# 读取长度
length_bits = bits[i+8:i+8+32]
length = 0
for j, bit in enumerate(length_bits):
length |= (bit << (31 - j))
# 读取文本
text_bits = bits[i+8+32:i+8+32+length*8]
text_bytes = bytearray()
for j in range(0, len(text_bits), 8):
byte = 0
for k in range(8):
byte |= (text_bits[j+k] << (7 - k))
text_bytes.append(byte)
# 验证结束标记
end_pos = i+8+32+length*8
end_marker_bits = [int(b) for b in format(0x55, '08b')]
if bits[end_pos:end_pos+8] == end_marker_bits:
print(f"提取水印: {text_bytes.decode('utf-8')}")
return
print("未检测到水印")
if __name__ == '__main__':
if len(sys.argv) != 2:
print("用法: python3 extract_watermark.py <image.png>")
else:
extract_lsb(sys.argv[1])
四、方案优势
| 特性 | 说明 |
|---|---|
| 无感性 | LSB水印肉眼完全不可见,不改变图片观感 |
| 源文件安全 | 只修改构建产物(DerivedData中的App Bundle),不触碰.xcassets原始图片 |
| 开关可控 | 通过Build Settings一键启用/禁用,不同配置灵活切换 |
| 自动化 | 集成到Xcode构建流程,无需手动操作 |
| 可追溯 | 支持从图片中提取水印,用于版权验证 |
| 轻量 | 仅依赖Python3 + Pillow,macOS自带环境,自动安装缺失库 |
五、注意事项
- 仅支持PNG:JPEG等有损压缩会破坏LSB水印,脚本会自动跳过非PNG文件。
- 性能影响:嵌入水印会增加构建时间(取决于图片数量和大小),建议仅在Release模式下启用。
- 图片容量 :水印文本长度+标记位约需
(8+32+文本长度*8+8)位。一张512x512的PNG可容纳约786432位(约96KB文本),完全满足需求。 - App Store合规:该水印不影响App功能,也未注入额外代码,符合App Store审核标准。
六、集成步骤
- 将
invisible_watermark.py放入项目Scripts/文件夹。 - 在Xcode Build Phases中添加Run Script,粘贴上述脚本内容。
- 在Build Settings中添加User-Defined Setting
ENABLE_INVISIBLE_WATERMARK,Release设为YES。 - 正常构建,水印自动嵌入。
此方案已用于多个生产项目,稳定可靠。