Image watermark library for MoonBit: visible and invisible traceable watermarks with embed/extract/verify APIs and a CLI.
| 类型 | API | 说明 |
|---|---|---|
| 文本水印 | RgbaImage::embed_text + TextConfig | 内置 5×7 点阵字体(A-Z / 0-9 / 常用符号)+ 16×16 中文点阵(Cjk16,GB2312 一级字 3755 个 + ASCII/符号 96 个,v0.1.10);TextConfig.font 切换,支持缩放、透明度、定位、角度旋转(angle,45° 斜向防盗播水印)。Cjk16 含汉字+ASCII:混合文本(中文标语+英文标识+数字/时间戳)一条水印完整渲染;生僻字/自定义品牌字形用 BitmapFont::with_cjk16(extra) 补充。越界防护(v0.1.11):水平文本超宽/超高默认报错,auto_shrink=true 自动缩小适配 |
| 随机点阵水印 | RgbaImage::embed_dots + DotConfig | 确定性 PRNG(xorshift64)按网格落点;同一 seed 完全可复现 → 可提取、可溯源 |
| Logo 水印 | RgbaImage::embed_logo | 任意 RGBA 图叠加,支持透明 PNG |
| 平铺水印 | RgbaImage::embed_tiled + TiledConfig | 全幅网格平铺,防截屏/盗摄 |
| API | 说明 |
|---|---|
| RgbaImage::embed_invisible + extract_invisible | LSB 逐位嵌入(RGB 通道各 1 bit/像素);key 派生 keystream 整体混淆,错误 key 在 magic 校验处被拒绝;容量 = 宽×高×3/8 字节 |
| RgbaImage::embed_dct + extract_dct | DCT 域:亮度域(BT.601 Y)8×8 分块 DCT-II,中频系数 (4,1) QIM 嵌入(与 JPEG 亮度分量一致,抗色度量化);delta 默认 24,JPEG 重压可提取(自然纹理 q85 默认通过;强压缩 q60 建议 delta=48);容量 = 完整 8×8 块数/8 字节 |
| API | 说明 |
|---|---|
| extract_dots | 用同一 DotConfig 复现点位置,统计深色命中率(0.0~1.0) |
| verify_dots | 命中率 ≥ 阈值(默认 0.9)判定水印存在 |
| verify_invisible | LSB 水印 magic 校验 |
| trace_dots | 对候选 DotConfig 数组溯源:返回命中率最高者索引(≥ 阈值);分发场景为每个接收者分配独立 seed,泄漏即锁定接收者 |
| extract_dct | DCT 水印 magic "MD" + 长度校验;JPEG/PNG 往返均可提取;delta 可选参数须与嵌入侧一致 |
| detect_lsb / detect_dct | 盲检测:不依赖 key/delta,仅读帧头 magic + 长度合理性判断"是否被标记"——溯源第一问(拿到疑似泄漏帧先判断,再决定深入提取);注意带 key 混淆的 LSB 帧无 key 检不出(文档化语义) |
| RgbaImage::lsb_capacity / dct_capacity | 查询各通道可用 payload 容量(字节,扣除帧头),嵌入前规划"这条 payload 走 LSB 还是 DCT" |
| RgbaImage::embed_all + EmbedOptions | 组合嵌入:一次调用按配置叠加文本/点阵/LSB/DCT(可选通道),面向逐帧应用(直播贴片)复用;LSB 与 DCT 互斥返回 Err,不静默降级 |
| API | 说明 |
|---|---|
| psnr(orig, marked) | 峰值信噪比(dB),量化嵌入前后可感知差异;尺寸不一致返回 None |
| 场景 | 推荐能力 | 机制 |
|---|---|---|
| 电商主图 / 详情页防同行盗图 | 可见(Logo / 文本) | 店铺名 + 商品信息角落或平铺,搬运必留痕 |
| 素材 / 图库预览防盗 | 可见 + 不可见 | 预览图盖半透明水印;样图嵌客户 ID |
| 内部设计稿 / 文档截图泄密溯源 | trace_dots | 每个员工 / 供应商独立 seed,外泄即定位 |
| 图片版权确权(摄影 / 插画 / 设计) | LSB / DCT 不可见 | 创作时嵌入作者 ID + 时间,维权时提取作证;经压缩分发时选 DCT |
| API 图片 / 头像接口追踪 | LSB / DCT 不可见 | 按调用方嵌入 ID,识别爬虫、盗链、二次分发;CDN 压缩链路选 DCT |
| 广告 / 营销素材渠道追踪 | trace_dots | 不同投放渠道不同 seed,泄漏渠道一测便知 |
| 社交 / UGC 平台防二创搬运 | 平铺 | 全幅网格水印,截图翻拍仍可见 |
| 直播 / 视频切片防搬运 | 可见 + 点阵(动态) | 平台名 + 时间戳 + 观众 ID,配合 OBS 浏览器源落地 |
| 报告 / 报表 / 合同截图防外泄 | 平铺 + 文本 | 公司名 + 部门,截屏即留痕 |
| 取证存证 | 文本 + LSB | 时间 + 拍摄者身份,形成证据链 |
moon add yuzhiblue/moon-watermark// 依赖:moon add yuzhiblue/moon-watermark
// import:
// import "yuzhiblue/moon-watermark" @lib
// import "moonbitlang/core/encoding/utf8" @utf8 // 中文 payload 转 UTF-8 字节
// import "moonbitlang/core/bytes" @bytes // Bytes 切片(decode_png 结果)
//
// 注意:String::to_bytes 在 MoonBit 中返回 UTF-16,不可直接作 payload,
// 必须经 @utf8.encode 转码(含 ASCII:@utf8.encode("UID-001"))。
// 1. 生成测试图(或 decode_png 解码真实图片)
let img = @lib.RgbaImage::new(320, 240, 0xFFFF_FFFF)
// 2. 组合嵌入:文本(右下角 45° 斜向)+ 点阵(seed=接收者 ID)+ LSB(观众 ID)
// embed_all 返回 Result(任一通道失败即 Err,不产生半成品)
// v0.1.12 起 with_font 可直接设 angle(45° 斜向防盗播水印)
let tcfg = @lib.TextConfig::with_font("LIVE", 8, 200, 2, 0.6, 0xFF00_00FF, "ascii5x7", angle=45.0)
let dcfg = @lib.DotConfig::default()
let opts : @lib.EmbedOptions = {
text_cfg: Some(tcfg),
dot_cfg: Some(dcfg),
lsb_payload: Some(@utf8.encode("UID-001")),
lsb_key: Some(42L),
dct_payload: None,
dct_delta: 24,
}
let marked = match img.embed_all(opts) {
Ok(m) => m
Err(e) => { println("embed failed: \{e}"); abort() }
}
// 3. 盲检测 → 提取验证(溯源链路:先 detect 再 extract)
if @lib.detect_lsb(marked) { println("marked ✓") }
let back = @lib.decode_png(marked.encode_png().unwrap_or(Bytes::new())).unwrap_or(img)
let rate = @lib.extract_dots(back, dcfg) // ≥0.9 判定点阵水印存在# 嵌入:文本 + 点阵 + LSB 不可见水印,输出 PNG/JPEG(按扩展名)
moon run cmd/main -- embed in.png out.png --text "LIVE 2026-10-03" --font cjk16 --lsb "UID-001" --key 42
# 只嵌 DCT 域水印(抗 JPEG),delta 加大可抗更强压缩
moon run cmd/main -- embed in.jpg out.jpg --dct "UID-001" --delta 48
# 验证
moon run cmd/main -- verify-dots out.png # 点阵命中率
moon run cmd/main -- verify-lsb out.png --key 42 # LSB 提取(key 必须与嵌入一致)
moon run cmd/main -- verify-dct out.png --delta 48 # DCT 提取(delta 必须与嵌入一致)
# 无参数 = 内存演示(原 Quick Start 示例)
moon run cmd/main


moon run cmd/e2e # 默认用 assets/live_frame.png
moon run cmd/e2e -- your.png # 任意图片跑同一链路
→ 
moon run cmd/bench # 320×240 … 1920×1080 四档
moon run cmd/bench -- 640 360 # 指定尺寸| 操作 | 320×240 | 640×360 | 1280×720 | 1920×1080 |
|---|---|---|---|---|
| embed_text(cjk16 右下角) | 85 µs | 85 µs | 85 µs | 110 µs |
| embed_dots(128 点) | 5.5 µs | 5.6 µs | 5.6 µs | 5.6 µs |
| embed_invisible(LSB) | 3.5 µs | 3.5 µs | 3.5 µs | 3.5 µs |
| embed_dct(delta=24) | 111 ms | 112 ms | 110 ms | 110 ms |
| embed_all(文本+点阵+LSB) | 95 µs | 98 µs | 94 µs | 118 µs |
| encode_png | 22 ms | 60 ms | 230 ms | 514 ms |
符号含义:✅ 通过/可提取 · ❌ 该攻击下提取失败(水印特性,见说明列)· ⚠️ 已知边界(点阵在纯裁剪下失效)。❌ 不是功能缺失——每种水印各有适用链路,表格与说明列给出替代方案。
| 攻击 | 点阵水印命中率 | LSB 不可见水印 | DCT 域水印 | 说明 |
|---|---|---|---|---|
| PNG 无损往返 | 1.0 ✅ | 可提取 ✅ | 可提取 ✅ | PNG 是 LSB 的可靠载体,DCT 亮度域无损往返亦稳定 |
| JPEG q60 重压 | 1.0 ✅ | 提取失败 ❌ | 可提取 ✅(delta=48) | 深色点高对比经 JPEG 保留;LSB 被量化破坏;DCT 在亮度域嵌入、与 JPEG 量化域一致(delta 越大越抗压,默认 24 适合 PNG 工作流与自然纹理 q85,重压缩建议 --delta 48) |
| 缩放至 1/2(Nearest) | 1.0 ✅ | 提取失败 ❌ | 提取失败 ❌ | 点阵:归一化网格 + 偶数对齐 + 邻域容差(v0.1.2 起)。LSB/DCT:像素坐标随缩放错位 → 失败;需抗几何攻击的不可见水印属后续规划 |
| 裁剪半幅 | 0.0 ⚠️ | 可提取 ✅ | 提取失败 ❌ | 点阵:归一化网格按新尺寸采样、而裁剪内容是原坐标像素 → 错位(已知边界)。LSB:裁剪保留原坐标像素 → 可提取。DCT:分块按原坐标对齐,裁剪错位。纯裁剪攻击建议配合 LSB |
pub trait GlyphProvider {
fn cell_width(Self) -> Int
fn cell_height(Self) -> Int
fn glyph(Self, Char) -> FixedArray[Byte]?
}impl GlyphProvider for BitmapFontfn BitmapFont::new(cell_w : Int, cell_h : Int, pairs : Array[(Char, FixedArray[Byte])]) -> BitmapFontpub struct DotConfig {
density : Double
dot_size : Int
seed : Int64
opacity : Double
grid_offset_x : Int
grid_offset_y : Int
}pub(all) struct EmbedOptions {
text_cfg : TextConfig?
dot_cfg : DotConfig?
lsb_payload : Bytes?
lsb_key : Int64?
dct_payload : Bytes?
dct_delta : Int
}let opts = {
text_cfg: Some(tcfg),
lsb_payload: Some(payload),
lsb_key: 7L,
dot_cfg: None,
dct_payload: None,
dct_delta: 24,
}pub struct RgbaImage {
width : Int
height : Int
data : FixedArray[Byte]
}fn RgbaImage::resize(self : RgbaImage, width : Int, height : Int, sampler : SampleMode) -> RgbaImagepub struct TextConfig {
text : String
x : Int
y : Int
font_size : Int
opacity : Double
angle : Double
color : Int
font : String
auto_shrink : Bool
}fn TextConfig::new(text : String, x : Int, y : Int, font_size : Int, opacity : Double, color : Int) -> TextConfigfn TextConfig::with_font(text : String, x : Int, y : Int, font_size : Int, opacity : Double, color : Int, font : String, angle? : Double) -> TextConfigpub struct TiledConfig {
text : String
font_size : Int
spacing_x : Int
spacing_y : Int
opacity : Double
color : Int
font : String
}fn TiledConfig::new(text : String, font_size : Int, spacing_x : Int, spacing_y : Int, opacity : Double, color : Int, font? : String) -> TiledConfigfn blend_pixel(dst : Int, src : Int, alpha : Int) -> Intfn[T : GlyphProvider] render_text_with(text : String, color : Int, provider : T, scale? : Int) -> RgbaImagefn xorshift64_next(state : UInt64) -> UInt64Install
Download zipImage watermark library for MoonBit: visible and invisible traceable watermarks with embed/extract/verify APIs and a CLI.