Sign in

    moon-watermark

    Image watermark library for MoonBit: visible and invisible traceable watermarks with embed/extract/verify APIs and a CLI.

    watermark
    image
    security
    anti-piracy
    steganography
    Download zip
    Author
    Version
    0.1.12
    License
    Apache-2.0
    Last updated
    5 days ago
    Downloads
    21

    Dependencies

    #moon-watermark

    MoonBit 图像水印算法库:可见水印(文本 / 随机点阵 / Logo / 平铺)+ 不可见溯源水印(LSB / DCT 域),提供 embed / extract / verify / trace API 与配置 JSON 序列化。内置中文点阵字形(Cjk16)与可扩展的 GlyphProvider 字形接口。定位为通用图像水印基础库——电商防搬运、素材预览防盗、图片版权确权、内部泄密溯源、API 图片追踪、直播防去重等场景开箱即用。

    #功能矩阵

    #可见水印(已实现)

    类型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_invisibleLSB 逐位嵌入(RGB 通道各 1 bit/像素);key 派生 keystream 整体混淆,错误 key 在 magic 校验处被拒绝;容量 = 宽×高×3/8 字节
    RgbaImage::embed_dct + extract_dctDCT 域:亮度域(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_invisibleLSB 水印 magic 校验
    trace_dots对候选 DotConfig 数组溯源:返回命中率最高者索引(≥ 阈值);分发场景为每个接收者分配独立 seed,泄漏即锁定接收者
    extract_dctDCT 水印 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,不静默降级

    #图像 IO(基于 mizchi/image)

    decode_png / encode_png / decode_jpeg / encode_jpeg / resize / crop / rotate(任意角度,逆映射最近邻;90/180/270 精确搬运)

    #不可见性度量(已实现)

    API说明
    psnr(orig, marked)峰值信噪比(dB),量化嵌入前后可感知差异;尺寸不一致返回 None

    实测(128×128 纹理测试图):LSB 不可见水印 PSNR 76.98dB(>40dB,肉眼不可察)· DCT 域水印 PSNR 50.9dB(delta=24)/ 45.3dB(delta=48)(亮度域嵌入,均 >40dB 肉眼不可察)· 点阵可见水印 25.07dB(明显可见,起威慑作用)。

    #配置持久化(已实现)

    text_config_to_json/from_json、dot_config_to_json/from_json、tiled_config_to_json/from_json(core @json,seed 以 Number 表示)

    #典型应用场景

    四种能力可按需组合,覆盖图片保护的常见诉求:

    场景推荐能力机制
    电商主图 / 详情页防同行盗图可见(Logo / 文本)店铺名 + 商品信息角落或平铺,搬运必留痕
    素材 / 图库预览防盗可见 + 不可见预览图盖半透明水印;样图嵌客户 ID
    内部设计稿 / 文档截图泄密溯源trace_dots每个员工 / 供应商独立 seed,外泄即定位
    图片版权确权(摄影 / 插画 / 设计)LSB / DCT 不可见创作时嵌入作者 ID + 时间,维权时提取作证;经压缩分发时选 DCT
    API 图片 / 头像接口追踪LSB / DCT 不可见按调用方嵌入 ID,识别爬虫、盗链、二次分发;CDN 压缩链路选 DCT
    广告 / 营销素材渠道追踪trace_dots不同投放渠道不同 seed,泄漏渠道一测便知
    社交 / UGC 平台防二创搬运平铺全幅网格水印,截图翻拍仍可见
    直播 / 视频切片防搬运可见 + 点阵(动态)平台名 + 时间戳 + 观众 ID,配合 OBS 浏览器源落地
    报告 / 报表 / 合同截图防外泄平铺 + 文本公司名 + 部门,截屏即留痕
    取证存证文本 + LSB时间 + 拍摄者身份,形成证据链

    选型速记:要看得见震慑 → 可见水印;要看不见取证 → LSB;图片要过 JPEG/压缩链 → DCT;要出事能定位人 → 按接收者分发 seed 走 trace_dots。

    #安装

    moon add yuzhiblue/moon-watermark

    #Quick Start

    完整可运行示例见 cmd/main/main.mbt(moon run cmd/main)。库外使用时需引入依赖与模块:

    // 依赖: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 判定点阵水印存在

    #命令行工具(文件模式 CLI)

    cmd/main 提供文件 IO 的 CLI(v0.1.7 起,基于 moonbitlang/x/fs 社区包):

    # 嵌入:文本 + 点阵 + 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

    要点:

    • --lsb 与 --dct 互斥:DCT 重写亮度分量会破坏 LSB,同时指定时 CLI 保留 LSB、跳过 DCT 并警告。
    • 嵌入与提取的 --delta 必须一致:DCT 提取按同一量化网格判定奇偶,两侧不一致会全部错位。
    • --opacity 同时作用于文本与点阵水印;文本默认放在右下角(避开不可见水印的左上角头部区),默认文本为 LIVE。
    • payload 按 UTF-8 编码:String::to_bytes 在 MoonBit 中返回 UTF-16,CLI 已用 @encoding/utf8 统一转码(含中文 payload)。
    • 水印组合顺序:可见水印(文本/点阵/Logo)先嵌、不可见水印最后(避免覆盖其左上角头部区——可见水印请避开头部区位置)。embed_all 中 LSB 与 DCT 互斥返回 Err(库 API 不静默降级;CLI 因保留 LSB 的取舍是跳过 DCT 并警告,两者策略不同)。

    #演示

    嵌入前后对比(2×2:左上原图 → 右上 LSB 不可见水印 → 左下可见水印 → 右下差异标记,红色点为 LSB 实际改动位置——512×288 图仅 28 px,占比 0.02%,肉眼不可察):

    嵌入前后对比

    溯源演示(三位接收者持不同 seed,泄漏帧经 trace_dots 锁定接收者 C):

    溯源演示

    鲁棒性矩阵可视化(数据来自 robustness_wbtest.mbt 实测):

    鲁棒性矩阵

    #真实场景链路演示(E2E)

    以一张模拟直播画面(assets/live_frame.png)跑通贴片嵌入 → JPEG 转码 → 溯源验证全链路,覆盖"直播去重 + 防盗播溯源"真实用法:

    moon run cmd/e2e # 默认用 assets/live_frame.png moon run cmd/e2e -- your.png # 任意图片跑同一链路

    链路与实测输出(moon run cmd/e2e,v0.1.10):

    1. 贴片三件套嵌入:可见文本 直播 UID-00421(中下部字幕条上方,防盗播震慑;cjk16 混合文本——中文+英文+数字一条水印完整渲染,v0.1.10;v0.1.11 起 48px 字号完整落在画布内)+ 点阵 seed=421(观众 ID,溯源用)+ DCT UID-00421(抗平台转码的取证载体);
    2. 模拟平台转码:encode_jpeg(85)(平台切片/分发必经的 JPEG 重压链路)→ 读回;
    3. 溯源验证:
      • extract_dots 命中率 1 → PASS(点阵在 JPEG q85 后完整保留);
      • extract_dct 提取 UID UID-00421 → 定位观众 421(DCT 域水印在亮度域嵌入,与 JPEG 量化域一致,转码后仍可提取);
      • trace_dots 对候选 [seed 1, 2, 421] 溯源 → 命中索引 2 → 锁定观众 C。

    原图 → 水印图(E2E 产物) → 贴片水印后

    转码产物 docs/demo/e2e_jpeg.jpg 即为"泄漏帧",可直接对其重复上述验证。

    #性能基准(实测)

    cmd/bench 可执行基准(core @bench.single_bench,10 样本自适应批量,中位数):

    moon run cmd/bench # 320×240 … 1920×1080 四档 moon run cmd/bench -- 640 360 # 指定尺寸

    实测(v0.1.10,MoonBit 0.10.14 native,渐变纹理测试图,payload 8 字节):

    操作320×240640×3601280×7201920×1080
    embed_text(cjk16 右下角)85 µs85 µs85 µs110 µs
    embed_dots(128 点)5.5 µs5.6 µs5.6 µs5.6 µs
    embed_invisible(LSB)3.5 µs3.5 µs3.5 µs3.5 µs
    embed_dct(delta=24)111 ms112 ms110 ms110 ms
    embed_all(文本+点阵+LSB)95 µs98 µs94 µs118 µs
    encode_png22 ms60 ms230 ms514 ms

    要点(与设计目标一致):

    • 嵌入成本与画面尺寸基本无关、与 payload 长度线性:DCT/LSB 只改写 payload 所需区域(12 字节帧 = 4B 头 + 8B payload = 96 bit:DCT 96 个 8×8 块 / LSB 32 像素),文本只渲染文本区域——贴片工坊逐帧调用 embed_all 的成本稳定在 ~100 µs 级,不随分辨率放大;
    • DCT 是最重路径(~110 ms,全 DCT-II 逐块变换,wasm 解释模式);逐帧实时流建议走 LSB/点阵组合,DCT 用于关键帧取证;
    • 编码 IO 随尺寸线性(PNG 1080p ~0.5 s),生产链路建议编码走并行或选择 JPEG(encode_jpeg(85) 远快于 PNG,且 DCT 水印本就抗 JPEG)。

    #鲁棒性测试矩阵(实测)

    moon test 的 robustness_wbtest.mbt 实测数据(128×128 测试图,DotConfig::default(),density=0.05):

    符号含义:✅ 通过/可提取 · ❌ 该攻击下提取失败(水印特性,见说明列)· ⚠️ 已知边界(点阵在纯裁剪下失效)。❌ 不是功能缺失——每种水印各有适用链路,表格与说明列给出替代方案。

    攻击点阵水印命中率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

    #设计说明

    • 颜色打包:0xRRGGBBAA。MoonBit Int 为 32 位有符号,0xFFFF_FFFF 等高位字面量会表示为负数——位运算完全等价,无需担心;推荐用 (r << 16) | (g << 8) | b | (a << 24) 构造颜色。
    • 确定性:点阵水印完全由 DotConfig(含 seed)决定,这是提取与溯源的基础。
    • 归一化网格:点阵落点按比例坐标(单元 = 图宽 8%),点坐标对齐偶数像素,因此缩放后仍可提取;代价是纯裁剪会因参考尺寸改变而错位(见鲁棒性矩阵)。
    • PNG 无损往返:PNG 编码/解码不损失 LSB 信息,是不可见水印的可靠载体(见鲁棒性矩阵)。
    • DCT 域设计:亮度域(BT.601 Y=0.299R+0.587G+0.114B) 8×8 分块 DCT-II + 中频系数 (4,1) QIM 量化嵌入,delta 默认 24(越大越抗 JPEG、可见性略升)。亮度域与 JPEG 编码的 Y 分量一致,DCT 系数才能经受色度量化往返(在 G 通道嵌入会被 YCbCr 色度量化破坏——v0.1.7 起改为亮度域)。注意:纯白/纯黑等饱和区域的正扰动会被像素 clamp 截断(系数跌到 Δ/2 边界),鲁棒性弱——真实图像纹理区不受影响;若已知图片大面积饱和,调大 delta 或改用 LSB。
    • 安全边界:LSB 与 DCT 水印提供隐蔽性与 JPEG 鲁棒性,不提供强加密/抗伪造——嵌入格式(magic、布局、系数位置)为公开知识,知道算法者可提取或覆盖水印;需要鉴权/防伪时,依赖持有方按 secret 管理嵌入参数(LSB 的 key、DCT 的 delta),并在上层做密钥分发与吊销。
    • embed_* 统一返回 Result[RgbaImage, String](v0.1.9 起):可见水印当前无失败路径(Ok 恒成立),不可见水印容量超限返回 Err;调用方统一一种错误处理模式。extract_* 返回 Bytes?(提取失败 None),extract_dots 返回命中率(0.0~1.0)。
    • 旋转实现:逆映射最近邻 + 像素中心坐标(round 定位源像素),90/180/270 为精确像素搬运;源范围外像素透明。embed_text 对 >512 字符返回 Err——超长文本旋转会分配 GB 级中间位图(资源保护,见「非目标」)。
    • GlyphProvider 字形接口:trait GlyphProvider { cell_width / cell_height / glyph },内置 Ascii5x7(拉丁,A-Z/0-9/常用符号)与 Cjk16(16×16 中文点阵,GB2312 一级字 3755 个 + ASCII/符号 96 个,Noto CJK 生成;未收录的字形返回 None → 渲染为空白)。混合文本(如 直播 LIVE 2026-10-04 UID-00421)单条水印完整渲染——直播贴片“中文平台名+英文标识+时间戳/观众 ID”不再需要拆两条文本。自定义字形:实现 trait 后走 render_text_with,或用 BitmapFont::new(w, h, pairs) 一行构造自带字表、BitmapFont::with_cjk16(extra) 在汉字+ASCII 基础上补充生僻字/品牌字形(v0.1.10 起含 ASCII 段);亦可经 TextConfig.font(JSON 兼容)切换内置字体。

    #非目标(明确边界)

    • 不提供强加密/防伪认证:LSB/DCT 提供隐蔽性与 JPEG 鲁棒性;鉴权、防伪、密钥分发与吊销由上层负责(见「设计说明·安全边界」)。
    • 不可见水印不抗几何攻击(缩放/裁剪后提取失败是水印原理边界,非 bug):可见点阵水印抗缩放、LSB 抗裁剪已覆盖主流链路(见鲁棒性矩阵)。
    • 不做视频/流式封装:逐帧应用(如直播贴片)在上层组合本库能力——embed_all 即面向逐帧复用设计。
    • 载体格式仅 PNG/JPEG:GIF/WebP/AVIF/BMP 不在计划内;多格式需求请在调用方转码后嵌入。
    • 不做超长文本水印:embed_text 对 >512 字符返回 Err(旋转中间位图可至 GB 级内存,边界测试固化)。

    #状态

    • 当前:v0.1.11 已发布(mooncakes,https://mooncakes.io/docs/yuzhiblue/moon-watermark);v0.1.12 在途:代码审查修复轮——embed_text 除零防护(font_size=0 + 超宽 + auto_shrink 不再崩溃)、CLI 文本水印失败显式警告、TextConfig::with_font 新增可选 angle(库外可设 45° 斜向水印)、LSB/DCT 重复逻辑合并(lsb_shift / default_delta / block_origin / map_from_pairs)、RgbaImage::new 字节填充提速。101 测试全过,moon check --deny-warn 零警告,CI 绿。

    #License

    Apache-2.0

    GlyphProvider

    pub trait GlyphProvider {
    fn cell_width(Self) -> Int
    fn cell_height(Self) -> Int
    fn glyph(Self, Char) -> FixedArray[Byte]?
    }

    字形提供者:按字符返回点阵位图。

    点阵约定:每行一个字节,低 cell_width 位为像素,位 cell_width - 1 为最左像素(与内置 5×7 表一致);行数 = cell_height。

    Ascii5x7

    pub enum Ascii5x7 {
    Ascii5x7
    }

    内置 5×7 ASCII 字形提供者。

    Ascii5x7::cell_height

    fn Ascii5x7::cell_height(_self : Ascii5x7) -> Int

    Ascii5x7::cell_width

    fn Ascii5x7::cell_width(_self : Ascii5x7) -> Int

    Ascii5x7::glyph

    fn Ascii5x7::glyph(_self : Ascii5x7, c : Char) -> FixedArray[Byte]?

    Ascii5x7::new

    fn Ascii5x7::new() -> Ascii5x7

    构造 Ascii5x7。

    BitmapFont

    pub struct BitmapFont {
    cell_w : Int
    cell_h : Int
    glyphs : Map[Char, FixedArray[Byte]]
    }

    通用位图字体:由调用方提供字形表,自动实现 [GlyphProvider]。

    用于自定义字形场景:客户用自己的工具(如"字体→点阵"转换脚本)生成 字形数据,一行构造即可用于水印渲染,无需手写 trait 实现。

    BitmapFont::cell_height

    fn BitmapFont::cell_height(self : BitmapFont) -> Int

    BitmapFont::cell_width

    fn BitmapFont::cell_width(self : BitmapFont) -> Int

    BitmapFont::glyph

    fn BitmapFont::glyph(self : BitmapFont, c : Char) -> FixedArray[Byte]?

    BitmapFont::new

    fn BitmapFont::new(cell_w : Int, cell_h : Int, pairs : Array[(Char, FixedArray[Byte])]) -> BitmapFont

    由调用方提供的字形对构造;pairs 中重复字符以后者为准。

    BitmapFont::with_cjk16

    fn BitmapFont::with_cjk16(extra : Array[(Char, FixedArray[Byte])]) -> BitmapFont

    以内置 Cjk16(GB2312 一级 3755 字)为基础,叠加调用方补充/覆盖的字形。 适合在通用字集上追加生僻字、专用字或品牌定制字形。 注意:内部会拷贝内置字表——补充的字形只影响返回的 BitmapFont, 不会污染全局 Cjk16 提供者(Map 为引用语义,直接 let m = cjk_glyphs 再 set 会改写全局字表)。

    Cjk16

    pub enum Cjk16 {
    Cjk16
    }

    内置 16×16 中文点阵提供者(GB2312 一级字 3755 个,覆盖常用汉字)。

    Cjk16::cell_height

    fn Cjk16::cell_height(_self : Cjk16) -> Int

    Cjk16::cell_width

    fn Cjk16::cell_width(_self : Cjk16) -> Int

    Cjk16::glyph

    fn Cjk16::glyph(_self : Cjk16, c : Char) -> FixedArray[Byte]?

    Cjk16::new

    fn Cjk16::new() -> Cjk16

    构造 Cjk16。

    DotConfig

    pub struct DotConfig {
    density : Double
    dot_size : Int
    seed : Int64
    opacity : Double
    grid_offset_x : Int
    grid_offset_y : Int
    }

    随机点阵水印配置。

    • density:点阵密度(0.0 ~ 1.0,越大点越密)。
    • dot_size:单个点的大小(像素边长)。
    • seed:确定性 PRNG 种子;同一种子在不同运行间生成相同水印(可复现、可验证)。
    • opacity:点的不透明度(0.0 ~ 1.0)。
    • grid_offset_x / grid_offset_y:点阵网格偏移,用于多平台差异化。

    DotConfig::default

    fn DotConfig::default() -> DotConfig

    默认配置:密度 0.05、点大小 3(1px 点在缩放/重压下会丢失,3px 兼顾可见性与鲁棒性)、 种子 42、全不透明、无偏移。

    DotConfig::new

    fn DotConfig::new(density : Double, dot_size : Int, seed : Int64, opacity : Double, grid_offset_x : Int, grid_offset_y : Int) -> DotConfig

    显式构造:供溯源场景为不同接收者分配不同 seed。

    EmbedOptions

    pub(all) struct EmbedOptions {
    text_cfg : TextConfig?
    dot_cfg : DotConfig?
    lsb_payload : Bytes?
    lsb_key : Int64?
    dct_payload : Bytes?
    dct_delta : Int
    }

    一次组合嵌入的完整配置。未启用的通道置 None:

    let opts = {
    text_cfg: Some(tcfg),
    lsb_payload: Some(payload),
    lsb_key: 7L,
    dot_cfg: None,
    dct_payload: None,
    dct_delta: 24,
    }

    EmbedOptions::default

    fn EmbedOptions::default() -> EmbedOptions

    全通道关闭的默认配置(各字段含义见 [EmbedOptions])。

    RgbaImage

    pub struct RgbaImage {
    width : Int
    height : Int
    data : FixedArray[Byte]
    }

    RGBA 图像数据结构:每像素 4 字节(R,G,B,A),行优先存储。

    内部用 FixedArray[Byte] 存储以便像素级原地修改(水印嵌入零复制); 与 mizchi/image 的 ImageData(Bytes)互转见 decode_png / encode_png。

    RgbaImage::crop

    fn RgbaImage::crop(self : RgbaImage, x : Int, y : Int, w : Int, h : Int) -> RgbaImage

    裁剪图像区域 (x, y, w, h)。越界坐标被钳制到图像范围内。

    RgbaImage::dct_capacity

    fn RgbaImage::dct_capacity(self : RgbaImage) -> Int

    查询 DCT 通道可用 payload 容量(字节):块数/8 再扣除 4 字节帧头。

    返回 0 或负数表示放不下(需更大的图或改走 LSB);容量与 embed_dct 的校验口径一致(4 + len <= blocks / 8)。

    RgbaImage::embed_all

    fn RgbaImage::embed_all(self : RgbaImage, opts : EmbedOptions) -> Result[RgbaImage, String]

    按配置组合嵌入水印并返回新图像(原图不变)。

    RgbaImage::embed_dct

    fn RgbaImage::embed_dct(self : RgbaImage, payload : Bytes, delta? : Int) -> Result[RgbaImage, String]

    RgbaImage::embed_dots

    fn RgbaImage::embed_dots(self : RgbaImage, cfg : DotConfig) -> Result[RgbaImage, String]

    随机点阵水印:按 DotConfig 的归一化网格 + PRNG 布局,在全图散布深色点。

    • 落点网格使用归一化坐标(单元边长 = 图宽 8%),点位置按比例跟随 图像尺寸——因此缩放(resize)后提取仍能命中,鲁棒性矩阵实测通过。
    • 每个单元由 seed 驱动的确定性 PRNG 决定是否落点(density 为落点 单元概率)及点在单元内的偏移比例(叠加 grid_offset_x / grid_offset_y 像素偏移,非零偏移在缩放场景会有轻微漂移,建议默认 0)。
    • 点颜色固定为深色(0x0000_00FF),半透明时由 opacity 控制; 同一配置(尤其同一 seed)嵌入结果完全一致,可被 extract_dots 复现。

    RgbaImage::embed_invisible

    fn RgbaImage::embed_invisible(self : RgbaImage, payload : Bytes, key? : Int64) -> Result[RgbaImage, String]

    将 payload(如观众 ID)以 LSB 方式嵌入图像(原地修改)。

    • 容量:width × height × 3 / 8 字节(含 4 字节头部);超出返回 Err。
    • key:可选混淆密钥,None 为明文嵌入。
    • 返回 Ok(图像) / Err(容量不足)。
    fn RgbaImage::embed_logo(self : RgbaImage, logo : RgbaImage, x : Int, y : Int, opacity : Double) -> Result[RgbaImage, String]

    Logo 水印:将 logo 图像叠加到 (x, y) 处(原地修改并返回同一图像)。

    • logo 的自身 alpha 参与混合(透明背景的 PNG 可直接使用)。
    • opacity(0.0~1.0)控制整体透明度;越界部分自动裁剪。

    RgbaImage::embed_text

    fn RgbaImage::embed_text(self : RgbaImage, cfg : TextConfig) -> Result[RgbaImage, String]

    在图像指定位置叠加文本水印(原地修改并返回同一图像)。

    位置由 TextConfig.x / y 指定(水印左上角);透明度仅由 opacity 控制(字形像素不透明);angle 按度旋转(逆时针, 0 = 不旋转,常用 45 做斜向防盗播水印)。 v0.1.11 起:水平/垂直文本(angle=0)越界不再静默裁剪——默认返回 Err,auto_shrink=true 时自动缩小字号适配画布;斜向文本(angle≠0) 保留跨画布特性(45° 防盗播水印需要延伸出画布)。

    RgbaImage::embed_tiled

    fn RgbaImage::embed_tiled(self : RgbaImage, cfg : TiledConfig) -> Result[RgbaImage, String]

    平铺水印:将 TiledConfig.text 渲染出的水印单元按间距平铺覆盖整张图 (原地修改并返回同一图像)。常用于防盗摄的全幅斜向/网格水印。

    RgbaImage::encode_jpeg

    fn RgbaImage::encode_jpeg(self : RgbaImage, quality : Int) -> Result[Bytes, String]

    将 RgbaImage 编码为 JPEG 字节(质量 0~100,用于鲁棒性测试)。

    RgbaImage::encode_png

    fn RgbaImage::encode_png(self : RgbaImage) -> Result[Bytes, String]

    将 RgbaImage 编码为 PNG 字节。

    RgbaImage::get_pixel

    fn RgbaImage::get_pixel(self : RgbaImage, x : Int, y : Int) -> Int

    读取像素,返回打包 RGBA 颜色 0xRRGGBBAA。

    RgbaImage::lsb_capacity

    fn RgbaImage::lsb_capacity(self : RgbaImage) -> Int

    查询 LSB 通道可用 payload 容量(字节):扣除 4 字节帧头。

    返回值为 0 或负数表示该图放不下任何 payload(嵌入会返回 Err)—— 嵌入前用本函数规划"这条 payload 走 LSB 还是 DCT"。

    RgbaImage::new

    fn RgbaImage::new(width : Int, height : Int, rgba : Int) -> RgbaImage

    创建指定尺寸的纯色图像。

    参数 rgba 为打包颜色:0xRRGGBBAA。

    RgbaImage::resize

    fn RgbaImage::resize(self : RgbaImage, width : Int, height : Int, sampler : SampleMode) -> RgbaImage

    缩放图像。

    注意:width / height 必须为正数——非法尺寸会 panic(而非返回 原图副本,避免调用方静默拿到错误结果);SampleMode 决定采样方式 (Nearest 最近邻 / Bilinear 双线性)。

    RgbaImage::rotate

    fn RgbaImage::rotate(self : RgbaImage, deg : Double) -> RgbaImage

    将图像绕中心旋转 deg 度(逆时针为正;任意角度,自动规范化到 [0, 360))。

    采用逆映射最近邻采样(像素中心坐标,round 定位源像素),源范围外的 像素保持透明(alpha 0)。输出为旋转后的包围盒尺寸;90/180/270 度为 精确像素搬运(无采样误差),其余角度为最近邻近似。

    RgbaImage::set_pixel

    fn RgbaImage::set_pixel(self : RgbaImage, x : Int, y : Int, rgba : Int) -> Unit

    写入像素,rgba 为打包颜色 0xRRGGBBAA。

    SampleMode

    pub(all) enum SampleMode {
    Nearest
    Bilinear
    }

    缩放采样方式。

    TextConfig

    pub struct TextConfig {
    text : String
    x : Int
    y : Int
    font_size : Int
    opacity : Double
    angle : Double
    color : Int
    font : String
    auto_shrink : Bool
    }

    文本水印配置。

    • x / y:水印左上角在目标图像中的坐标(像素)。
    • font_size:点阵字体的缩放倍数(1 = 原始 5×7 尺寸)。
    • opacity:整体不透明度(0.0 ~ 1.0)。
    • angle:旋转角度(度,逆时针,0 = 不旋转;常用 45 做斜向防盗播水印)。
    • color:打包 RGBA 颜色 0xRRGGBBAA。

    TextConfig::new

    fn TextConfig::new(text : String, x : Int, y : Int, font_size : Int, opacity : Double, color : Int) -> TextConfig

    便捷构造器:angle 默认为 0(不旋转)。

    TextConfig::with_font

    fn TextConfig::with_font(text : String, x : Int, y : Int, font_size : Int, opacity : Double, color : Int, font : String, angle? : Double) -> TextConfig

    带字形选择的构造器:font 取 "ascii5x7" 或 "cjk16"; angle 可选(度,逆时针,默认 0.0,常用 45 做斜向防盗播水印)——v0.1.12 起 库外调用方可直接设置旋转角(此前 angle 仅能 record 构造,外部无法指定)。

    TiledConfig

    pub struct TiledConfig {
    text : String
    font_size : Int
    spacing_x : Int
    spacing_y : Int
    opacity : Double
    color : Int
    font : String
    }

    平铺水印配置:text 按 font_size 放大为单元,以 spacing_x / spacing_y 为间距平铺覆盖整张图。

    TiledConfig::new

    fn TiledConfig::new(text : String, font_size : Int, spacing_x : Int, spacing_y : Int, opacity : Double, color : Int, font? : String) -> TiledConfig

    便捷构造器(颜色打包 RGBA 0xRRGGBBAA)。

    blend_pixel

    fn blend_pixel(dst : Int, src : Int, alpha : Int) -> Int

    标准 alpha 混合:将 src 以不透明度 alpha 叠加到 dst 上。

    逐通道计算:out = (src * a + dst * (255 - a)) / 255, 输入输出均为打包 RGBA 颜色 0xRRGGBBAA,alpha 取值 0~255。

    decode_jpeg

    fn decode_jpeg(raw : Bytes) -> Result[RgbaImage, String]

    从 JPEG 字节解码为 RgbaImage。

    decode_png

    fn decode_png(raw : Bytes) -> Result[RgbaImage, String]

    从 PNG 字节解码为 RgbaImage。

    detect_dct

    fn detect_dct(img : RgbaImage, delta? : Int) -> Bool

    盲检测:用给定 delta 读 DCT 帧头(magic + 长度合理性)判断是否含水印。

    delta 需与嵌入时一致(可选,默认 24)——detect 与 extract 共用同一 量化网格判定,两侧不一致会检不出(返回 false,属正常语义而非 bug)。 用于拿疑似泄漏帧先判断是否被 DCT 标记,再决定深入提取。

    detect_lsb

    fn detect_lsb(img : RgbaImage) -> Bool

    盲检测:不依赖 key,仅读帧头 magic + 长度合理性判断图像是否含 LSB 水印。

    与 extract_invisible 的区别:不需要 key 即可回答"是否被标记",适合 拿到疑似泄漏帧时先判断、再决定是否用正确 key 深入提取(溯源第一问)。 注意:若水印以非默认 key 混淆,magic 同样被 XOR,无 key 时可能检不出 (此时用 extract_invisible(img) 无 key 探测或先跑一次 key 枚举)。

    dot_config_from_json

    fn dot_config_from_json(s : String) -> DotConfig?

    从 JSON 字符串解析 DotConfig;字段缺失或格式错误返回 None。

    dot_config_to_json

    fn dot_config_to_json(cfg : DotConfig) -> String

    将 DotConfig 序列化为 JSON 字符串。

    extract_dct

    fn extract_dct(img : RgbaImage, delta? : Int) -> Bytes?

    从图像提取 DCT 域水印;magic 校验失败返回 None。

    extract_dots

    fn extract_dots(img : RgbaImage, cfg : DotConfig, brightness_threshold? : Int) -> Double

    提取点阵水印:用同一 cfg 复现点位置,统计深色命中率(0.0~1.0)。

    命中判定:像素亮度(RGB 均值)低于 brightness_threshold 视为命中。 用于验证水印是否存在 / 匹配某个 seed(溯源时不同接收者持有不同 seed)。

    extract_invisible

    fn extract_invisible(img : RgbaImage, key? : Int64) -> Bytes?

    从图像提取 LSB 不可见水印;magic 校验失败(含错误 key)返回 None。

    psnr

    fn psnr(orig : RgbaImage, marked : RgbaImage) -> Double?

    不可见性度量:PSNR(峰值信噪比,dB)。

    量化水印嵌入前后图像的可感知差异——LSB 不可见水印通常 >40dB(肉眼 不可察),可见水印显著低于此值。评审/验收时可直接用数字说明"嵌入 是否可感知"。

    render_text

    fn render_text(text : String, color : Int, scale? : Int) -> RgbaImage

    将文本渲染为 RGBA 位图。

    • color:字形颜色,打包 RGBA 0xRRGGBBAA;字形像素不透明时 alpha 取 255。
    • scale(可选,默认 1):点阵放大倍数,用最近邻放大。
    • provider(可选,默认 Ascii5x7):字形提供者,内置 Cjk16 支持中文点阵。
    • 字符间距 1 像素(按各字符单元宽度累加,支持中英文混排); 不支持的字形渲染为空白。
    • 空文本返回 0×0 图像。

    render_text_with

    fn[T : GlyphProvider] render_text_with(text : String, color : Int, provider : T, scale? : Int) -> RgbaImage

    带字形提供者的渲染(供自定义字形/Cjk16 使用)。

    seed_to_state

    fn seed_to_state(seed : Int64) -> UInt64

    将 seed(Int64)转换为 PRNG 初始状态。

    text_config_from_json

    fn text_config_from_json(s : String) -> TextConfig?

    从 JSON 字符串解析 TextConfig;字段缺失或格式错误返回 None。

    text_config_to_json

    fn text_config_to_json(cfg : TextConfig) -> String

    将 TextConfig 序列化为 JSON 字符串。

    tiled_config_from_json

    fn tiled_config_from_json(s : String) -> TiledConfig?

    从 JSON 字符串解析 TiledConfig;字段缺失或格式错误返回 None。

    tiled_config_to_json

    fn tiled_config_to_json(cfg : TiledConfig) -> String

    将 TiledConfig 序列化为 JSON 字符串。

    trace_dots

    fn trace_dots(img : RgbaImage, candidates : Array[DotConfig], threshold? : Double) -> Int

    溯源:对候选 DotConfig 逐一提取命中率,返回最高命中率候选的索引。

    候选为空或最高命中率低于 threshold(默认 0.9)时返回 -1。 直播溯源场景:每个接收者持有不同 seed 的配置,泄漏视频命中哪个 seed 即锁定对应接收者。

    verify_dots

    fn verify_dots(img : RgbaImage, cfg : DotConfig, threshold? : Double) -> Bool

    验证点阵水印是否存在:extract_dots 命中率 ≥ threshold(默认 0.9)。

    verify_invisible

    fn verify_invisible(img : RgbaImage, key? : Int64) -> Bool

    验证 LSB 不可见水印是否存在:能通过 magic 校验(含正确 key)即存在。

    xorshift64_next

    fn xorshift64_next(state : UInt64) -> UInt64

    确定性 64 位 PRNG(xorshift64)。

    相同种子产生完全相同序列,保证点阵水印可复现、可提取; 种子 0 会被替换为 1,避免全零状态死循环。