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.18
    License
    Apache-2.0
    Last updated
    2 days ago
    Downloads
    44

    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);注意这是绝对命中率,暗色调原图天然偏高,严谨判别请用 dot_contrast / verify_dots
    dot_contrast多对照成对信号(v0.1.18):目标点位命中率 − median(k 组对照点位命中率),controls 默认 k=8(设 1 则与单对照完全一致)。对照组由 seed 确定性派生(同密度、未画点处),同一张图上两侧用同一亮度阈值统计 → 原图暗区/纹理的系统性偏差被自动扣除。未嵌入图 contrast≈0(暗图不再假阳性);已嵌入图 contrast 显著 >0。标定数据:单对照在渐变/棋盘等结构图型上存在 1.3% 虚警(1024 样本实测 13 例),多对照压至 0.5%(5 例),自标定后 0.1%(1 例)——见 cmd/calibrate
    verify_dotsdot_contrast(多对照)≥ 阈值(默认 0.4)判定水印存在;未嵌入亮图/暗图均判 false,嵌入后判 true。calibrated=true 启用图像自标定显著性检验:contrast 还须显著高于本图 null 本底(+significance_margin,默认 0.15)才判存在——用于压制渐变等目标点位自身偏斜的残余虚警(v0.1.15 起由"绝对命中率 ≥0.9"升级,v0.1.18 升级为多对照+可选自标定)
    verify_invisibleLSB 水印 magic 校验
    trace_dots对候选 DotConfig 数组逐一算多对照 dot_contrast,返回信号最强且与次强差距 ≥ margin(默认 0.15)的候选索引;-1=无信号达标,-2=候选歧义不误锁,≥0=唯一强指向候选。calibrated=true 时最高候选还须显著高于本图 null 本底才指认。输出语义是统计证据而非最终裁决:最终归属判定应结合分发台账交叉核验。分发场景为每个接收者分配独立 seed(v0.1.15 引入,v0.1.18 升级为多对照+可选自标定)
    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 contrast = @lib.dot_contrast(back, dcfg) // 成对对照信号:目标命中率 − 对照命中率
    let ok = @lib.verify_dots(back, dcfg) // ≥0.4 判定点阵水印存在(v0.1.15)

    #命令行工具(文件模式 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 # 点阵成对对照信号 + 判定(contrast / verify) 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.15):

    1. 贴片三件套嵌入:可见文本 直播 UID-00421(中下部字幕条上方,防盗播震慑;cjk16 混合文本——中文+英文+数字一条水印完整渲染,v0.1.10;v0.1.11 起 48px 字号完整落在画布内)+ 点阵 seed=421(观众 ID,溯源用)+ DCT UID-00421(抗平台转码的取证载体);
    2. 模拟平台转码:encode_jpeg(85)(平台切片/分发必经的 JPEG 重压链路)→ 读回;
    3. 溯源验证(v0.1.15 起点阵判定升级为成对对照):
      • dot_contrast 0.5 + verify_dots → PASS(JPEG q85 后水印信号仍高于同图对照点位,原图暗区干扰已被扣除);
      • extract_dct 提取 UID UID-00421 → 定位观众 421(DCT 域水印在亮度域嵌入,与 JPEG 量化域一致,转码后仍可提取);
      • trace_dots 对候选 [seed 1, 2, 421] 溯源 → 唯一命中索引 2 → 锁定观众 C(信号最高且与次高差距 ≥ margin,无歧义不误锁)。

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

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

    #溯源判定:输入输出案例集

    点阵水印来源指认的判定规则,用五个可复现案例说明:如何区分原图干扰、候选歧义与真实分发来源,结构图型虚警如何被两级机制抑制,以及盲检测在溯源链路中的角色。以下输出由 moon run cmd/review 一键复现(判据实现见 dots.mbt::dot_contrast 与 verify.mbt::trace_dots,误报率与锁定质量实测见 cmd/calibrate,回归测试见 verify_wbtest.mbt 与 calibration_wbtest.mbt)。

    #判定规则总览

    判定问题区分机制判定出口案例
    原图干扰多对照成对信号 dot_contrast = 目标点位命中率 − median(k=8 组对照命中率)。对照组由 seed 确定性派生(同密度、未画点处),同一张图上两侧用同一亮度阈值统计 → 原图暗区/纹理的系统性偏差在差值中自动扣除verify_dots=false、trace_dots=-1(无候选达标)A
    候选歧义trace_dots 要求最高信号与次高信号差距 ≥ margin(默认 0.15)才锁定;不足即拒绝返回 -2:候选歧义,不误锁,转人工复核B
    真实分发来源分发时为每个接收者分配独立 seed;泄漏帧上只有真实来源候选的 contrast 显著 > 0(错误 seed ≈ 0 甚至为负),且与次强候选拉开 ≥ margintrace_dots 返回唯一索引(≥ 0)C
    盲检测(溯源第一问)detect_lsb/detect_dct 不依赖 key/delta,仅读帧头 magic + 长度合理性,回答「是否被标记」,再决定是否深入提取未标记 → 溯源终止;已标记 → 进入提取(需正确 key/delta)D
    结构图型虚警单对照在渐变/棋盘等图型上有残余虚警(目标点位子集偏斜):第一级多对照 k=8 压制对照侧噪声;第二级 calibrated=true 自标定显著性检验——候选须高于本图 null 本底 + margin 才输出两级修复后 verify=false / trace=-2(不输出指认)E

    #机制承诺边界(实测标定,cmd/calibrate 可复现)

    指标单对照(v0.1.17)多对照 k=8(v0.1.18 默认)+ 自标定(calibrated=true)
    未嵌入图误报率(1024 样本 × 16 图型)13/1024 ≈ 1.3%5/1024 ≈ 0.5%1/1024 ≈ 0.1%
    最大虚警 contrast0.6670.567—
    适用场景平滑内容画面平滑内容画面渐变/棋盘等结构图型兜底

    语义声明:trace_dots 的 ≥0 输出是「唯一强指向候选」,属可复核的统计证据,不构成"算法单独定案";最终归属判定 = 库输出证据 × 持有方分发台账交叉核验。点阵通道拒绝域(只报"无法确定",不输出指认):原图背景暗像素占比 >60%、高频二值纹理(噪声图 / 密集文字截图)、强几何变换(裁剪 / 旋转 / 缩放)——此类场景请改走 DCT/LSB 通道并以上层台账兜底。

    #实际运行输出(moon run cmd/review)

    ================================================================ moon-watermark 溯源判定案例集(多对照信号 + 自标定,v0.1.18) 点阵溯源的判定情形:原图干扰 / 候选歧义 / 真实分发来源 / 结构图型虚警 ================================================================ -------------------------------------------------------------- 案例 A:原图干扰(未嵌入水印的暗色原图 → 必须拒判) -------------------------------------------------------------- A1 输入:64×64 纯暗图(未嵌入任何水印),候选 seed=42 绝对命中率(旧判据)= 1 ← 旧判据在此假阳性 verify=true dot_contrast(新判据)= 0 verify_dots = false trace_dots = -1 → -1 = 无候选达标,不误锁(正确) A2 输入:64×64 暗纹理图(亮度 20/150 伪随机,未嵌入),候选 seed=42 绝对命中率(旧判据)= 1 ← 暗纹理系统抬高命中率 dot_contrast(新判据)= 0 verify_dots = false trace_dots = -1 → 原图干扰被对照项扣除,正确拒判 结论:原图干扰由成对对照机制自动扣除——未嵌入图 contrast ≈ 0, verify_dots=false、trace_dots=-1;假阳性在回归测试中固化。 -------------------------------------------------------------- 案例 B:候选歧义(信号不可区分 → -2 拒绝误锁;可区分 → 唯一命中) -------------------------------------------------------------- B1 输入:嵌入 seed=42 的图,候选 = [cfg42, cfg42](信号并列) 候选[0] seed=42 dot_contrast=1 达标(阈值 0.4) 候选[1] seed=42 dot_contrast=1 达标(阈值 0.4) 最高 − 次高 = 0 < margin(0.15) → trace_dots = -2 → -2 = 候选歧义,无法唯一确定来源,转人工复核(不误锁) B2 输入:同一张图,候选 = [cfg46(错误seed), cfg42(正确seed)] 候选[0] seed=46 dot_contrast=0 未达标(阈值 0.4) 候选[1] seed=42 dot_contrast=1 达标(阈值 0.4) → trace_dots = 1 → 唯一命中索引 1(正确 seed) 结论:能区分时差距 > margin 即锁定;不能区分时返回 -2—— 「歧义」与「命中」由同一阈值体系统一判定,无中间模糊态。 -------------------------------------------------------------- 案例 C:真实分发来源(分发独立 seed → 泄漏帧唯一锁定) -------------------------------------------------------------- C 输入:嵌入 seed=421(观众C)的图 → JPEG q85 转码 → 读回 候选 = [seed=1(观众A), seed=2(观众B), seed=421(观众C)] 候选[0] seed=1 dot_contrast=0 未达标(阈值 0.4) 候选[1] seed=2 dot_contrast=0 未达标(阈值 0.4) 候选[2] seed=421 dot_contrast=1 达标(阈值 0.4) verify_dots = true(JPEG q85 后仍通过) trace_dots = 2(0=观众A 1=观众B 2=观众C)→ 锁定观众 C(seed 421)——泄漏来源唯一确定 结论:分发时为每个接收者分配独立 seed,泄漏帧的 contrast 只在 真实来源候选上显著 >0 → trace_dots 返回其索引,溯源到人。 -------------------------------------------------------------- 案例 D:盲检测(溯源第一问:先判「是否被标记」再提取) -------------------------------------------------------------- D1 输入:未嵌入的原图 detect_lsb=false detect_dct=false → 未标记,溯源终止 D2 输入:嵌入 DCT 水印(UID-00421, delta=24)的图 detect_dct=true detect_lsb=false → 已标记(DCT)→ extract_dct = UID-00421 D3 输入:嵌入 LSB 水印(UID-001, key=42)的图 detect_lsb(无key)=false ← 带 key 混淆的帧头不可见(已文档化语义) extract_invisible(错误key=43) None(magic 校验拒绝,错误 key 提取失败);extract_invisible(正确key=42) = UID-001 结论:盲检测回答「是否被标记」(第一问,无需参数);提取回答 「标记内容是什么」(需正确 key/delta)。带 key 的 LSB 帧 以持有方嵌入记录为溯源入口,边界已在 README 安全边界声明。 -------------------------------------------------------------- 案例 E:结构图型虚警(渐变原图 → 多对照 + 自标定两级拒判) -------------------------------------------------------------- 输入:128×128 水平渐变图(0→255,未嵌入),候选 seed=12 / 8 E1 seed=12 单对照=0.4444444444444444(≥0.4 虚警) 多对照(k=8)=0.18308080808080807 → verify_dots=false(修复) E2 seed=8 单对照=0.6666666666666666(≥0.4 虚警) 多对照(k=8)=0.4833333333333333(仍达标——目标点位自身偏斜) calibrated=true 自标定:0.4833333333333333 未显著高于本图 null 本底 + 0.15 → verify_dots=false(修复) verify_dots(calibrated=true) = false 结论:单对照在结构图型上存在 ~1.3% 虚警(1024 样本实测 13 例); 多对照压至 0.5%,自标定压至 0.1%。残余虚警由显著性检验兜底—— 机制的承诺边界以 cmd/calibrate 标定数据为准,不做绝对化表述。 ================================================================ 判定规则速查(trace_dots 返回值语义): >= 0 唯一命中 → 锁定该候选对应的分发来源(输入索引) = -1 无候选达标 → 原图未嵌入或候选均不匹配(原图干扰已扣除) = -2 候选歧义(最高 − 次高 < margin 0.15)→ 不误锁,转人工复核 ================================================================

    #关键数值解读

    • 案例 A(原图干扰):纯暗图 / 暗纹理图未嵌入时,旧判据「绝对命中率 ≥ 0.9」= 1 → 必然假阳性(v0.1.14 及以前的已知缺陷);成对对照 contrast = 0 → 正确拒判。修复已在 verify_wbtest.mbt 的暗图假阳性测试组中固化(纯黑图、20/150 暗纹理图均 verify=false、trace=-1)。
    • 案例 B(候选歧义):两个相同候选 contrast 并列 = 1,最高 − 次高 = 0 < margin(0.15) → -2;对照组中错误 seed contrast = 0 vs 正确 seed = 1,差距 > margin → 唯一命中。「歧义」与「命中」由同一阈值体系统一判定,不存在"大致锁定"的中间态。
    • 案例 C(真实分发来源):JPEG q85 平台转码后,错误候选 contrast = 0(多对照中位数下,错误 seed 的目标命中率与对照命中率持平;单对照下甚至出现 −0.11 的负值——对照点位比目标点位更暗),真实来源 contrast = 1 → trace_dots = 2,指向观众 C。判据依赖"信号强度排序 + 差距"双条件,而非简单比零。
    • 案例 D(盲检测):未嵌入图全部无参检出为 false;DCT 帧可无参检出并提取内容;带 key 的 LSB 帧无 key 检不出(帧头被 keystream 混淆掩盖,已文档化语义,非缺陷)——此类帧以持有方嵌入记录为溯源入口,边界见「设计说明·安全边界」。
    • 案例 E(结构图型虚警):渐变图上 seed=12/8 单对照 contrast = 0.444 / 0.667 ≥ 0.4(虚警)——原图未嵌入却被判"存在水印",这是 v0.1.17 单对照机制的实测弱点。多对照(k=8)把 seed=12 修到 0.183;seed=8 因偏差出在目标点位自身仍 0.483,由 calibrated=true 自标定兜底拒判。这是"用数据校准承诺"的直接体现:机制能做什么、不能做什么,以 cmd/calibrate 的实测数据为准。

    #性能基准(实测)

    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)决定,这是提取与溯源的基础。
    • 多对照判定与自标定(v0.1.18):v0.1.15 的单对照在结构图型上存在残余虚警(渐变/棋盘/色块等 1024 样本实测 13 例,max 0.667)。dot_contrast 升级为多对照:目标命中率 − median(k=8 组对照命中率),controls 参数可回退 k=1(与旧实现逐位一致)。verify_dots / trace_dots 新增 calibrated 自标定模式——候选信号须显著高于本图 null 本底(8 个独立 seed 的 contrast 最大值 + margin)才输出判定。标定数据:单对照 1.3% → 多对照 0.5% → 自标定 0.1%(详见「溯源判定」章节与 cmd/calibrate)。
    • 点阵适用边界(如实说明):点阵是亮度对比通道——两类画面属拒绝域:① 原图背景暗像素占比过高(约 >60%,如整图纯暗),深色点与背景对比不足、contrast 偏低;② 高频二值纹理(噪声图、密集文字截图),3×3 邻域命中统计被纹理饱和,信号淹没在本底中。拒绝域内 trace_dots 只报"无法确定",不输出指认——请改走 DCT/LSB 通道(DCT 抗 JPEG,e2e 正是一起嵌入、互为兜底)。
    • 归一化网格:点阵落点按比例坐标(单元 = 图宽 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.18 已发布(mooncakes,https://mooncakes.io/docs/yuzhiblue/moon-watermark):判定机制标定升级——针对标定实验发现的单对照残余虚警(结构图型 1024 样本实测 13 例,max 0.667),dot_contrast 升级为多对照组(k=8 median,controls 可回退 k=1 向后兼容),verify_dots / trace_dots 新增 calibrated 自标定显著性检验(候选须显著高于本图 null 本底);误报率实测 1.3% → 0.5%(多对照)→ 0.1%(自标定)。新增 cmd/calibrate 标定实验(误报率 / 锁定质量可复现测量)与案例 E(结构图型虚警两级修复);溯源输出语义明确为「唯一强指向候选 / 无法确定」两种统计证据出口,最终归属判定结合分发台账交叉核验。114 测试全过(含 5 个标定回归测试),README 如实记录机制承诺边界与拒绝域。
    • v0.1.17:README 文案中性化与版本状态修正(章节更名「溯源判定:输入输出案例集」,版本状态与 mooncakes / GitHub Release 三方对齐)。
    • v0.1.16:moon.mod 版本号补位至 0.1.16 并发布 mooncakes(发布内容与 0.1.15 代码一致,解决 0.1.15 提交时 moon.mod 未同步的问题)。
    • v0.1.15:点阵判定从"绝对命中率"升级为成对对照信号(dot_contrast = 目标点位命中率 − 对照点位命中率)——对照点位由 seed 确定性派生(同密度、未画点处),同一图上同阈值统计,原图暗区/纹理干扰被自动扣除。修复暗色原图假阳性(纯黑/深纹理未嵌入图此前 verify=true、trace 误锁,现 contrast=0、verify=false);trace_dots 新增歧义防护(信号最高但与次高差距 < margin 返回 -2,不误锁);verify_dots 默认阈值 0.4、trace_dots 新增 margin 参数。同步升级 e2e/gen_demo/CLI 输出与回归测试(109 测试全过),并如实文档化点阵适用边界(整图过暗时点阵信号不可分辨,暗色画面走 DCT/LSB 通道兜底)。
    • v0.1.14:全项目代码审查修复——修复 Cjk16 中文渲染错位(渲染循环按每行字节数通用化,此前 16×16 中文/宽字形左半恒空、右半错位,"啊/防"等汉字首次正确显示);旋转方向文档统一为顺时针(与实现一致);DCT delta<=0 拒绝(此前静默产生无法提取的水印并改坏像素);LSB/DCT payload >65535 拒绝(2 字节长度字段防静默截断);点阵 density clamp 到 [0,1];DCT 余弦表预计算(消除每块上万次 math.cos 调用)。106 测试全过,moon check --deny-warn 零警告。
    • v0.1.13:工具链兼容与依赖修复——适配新版 moonc(Int → Int32、moon fmt 规范);移除多余依赖 mizchi/json;显式声明 moonbitlang/async@0.22.4,覆盖传递依赖的旧格式 async@0.16.6,使新版工具链下 CI 恢复全绿。
    • 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 字节填充提速;附带 Noto CJK(SIL OFL 1.1)许可全文与归属声明。

    #License

    moon-watermark 主体代码以 Apache-2.0 发布(见根目录 LICENSE)。

    Cjk16 字形数据派生自 Noto Sans CJK(Copyright 2014-2021 Adobe,SIL OFL 1.1):仓库未分发字体文件本身,仅分发由其渲染生成的 16×16 点阵数据(命名为 Cjk16,未使用 Reserved Font Name);许可全文见 fonts/OFL-1.1.txt,第三方归属声明见 THIRD_PARTY_NOTICES.md。

    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 字符串。

    dot_contrast

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

    成对对照判定(多对照组):目标点位命中率 − median(k 组对照点位命中率)。

    • 目标点位:cfg.seed 派生的落点(即嵌入者实际画点处);
    • 对照点位:由 cfg.seed 确定性派生的 k 组同密度随机落点 (第 j 组参照 seed = seed ^ (GOLDEN * j),j=1..k;k=1 时与单对照 完全一致)。

    两者在同一张图上用同一亮度阈值统计,原图的暗区/纹理等系统性偏差 同时作用于两侧,差值即"水印带来的真实信号":

    • 未嵌入图(含暗色原图):目标命中率 ≈ 对照命中率 → contrast ≈ 0;
    • 已嵌入图:目标点位与真水印重合(全暗)→ contrast 显著 > 0。

    为什么默认 k=8:单对照组在空间结构图型(渐变、棋盘、色块)上存在 残余方差——目标点位子集与个别对照子集的网格分布差异可造成 ≥0.4 的 虚警(标定实验实测 17/1024 ≈ 1.7% 误报)。取 k 组对照的中位数可压制 对照侧噪声,实测将误报降至 5/1024 ≈ 0.5%(见 cmd/calibrate)。 注意:偏差也可能来自目标点位自身(如渐变图上目标子集偏于一侧), 此时多对照无法完全消除——此类图型建议配合 verify_dots 的 calibrated 自标定模式,或列为点阵通道拒绝域。

    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 视为命中。 注意:这是绝对命中率,不含基线对照——暗色调原图本身可能天然 高命中(假阳性)。需要严谨判别的场景请用 verify_dots / dot_contrast (成对对照,自动扣除原图暗区干扰)。

    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, margin? : Double, controls? : Int, calibrated? : Bool) -> Int

    溯源:对候选 DotConfig 逐一计算多对照成对信号(dot_contrast), 返回信号最强且与次强之间有明确差距的候选索引。

    • 返回 -1:无候选的信号达到 threshold(默认 0.4)——原图未嵌入 或候选均不匹配(含暗色原图干扰已被 contrast 扣除的情形);
    • 返回 -2:存在 ≥ threshold 的候选,但最高与次高信号差距 < margin(默认 0.15)——候选歧义,无法唯一确定来源,需人工复核; 或 calibrated=true 时最高信号未显著超过本图本底;
    • 返回 >= 0:唯一命中候选索引(真实分发来源)。

    calibrated=true 时启用图像自标定显著性检验:最高候选还须超过 该图自身本底(8 个独立 null seed 的 contrast 最大值 + margin)才输出 指认,否则返回 -2——压制渐变/棋盘等目标点位自身偏斜导致的误锁。

    输出语义:>=0 表示「唯一强指向候选」,是统计证据而非最终裁决; 最终归属判定应结合持有方分发台账交叉核验。直播溯源场景:每个接收者 持有不同 seed 的配置,泄漏视频命中哪个 seed 即指向对应接收者; 歧义时宁可报告"无法确定"也不误锁。

    verify_dots

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

    验证点阵水印是否存在:多对照成对信号(dot_contrast)≥ threshold(默认 0.4)。

    与旧版"绝对命中率 ≥ 0.9"的区别:contrast 扣除同图 k=8 组对照点位的 中位数命中率,暗色调原图(直播暗场/夜景)与空间结构图型的背景偏差 不再产生假阳性(标定实验:单对照误报 1.7% → 多对照 0.5%)。 默认阈值 0.4:无损/轻压缩场景 contrast≈1.0,JPEG q85 后≈0.5,未嵌入≈0。 calibrated=true 时启用图像自标定显著性检验:除固定阈值外,还要求 contrast 超过该图自身本底(8 个独立 null seed 的 contrast 最大值 + significance_margin,默认 0.15)——用于渐变/棋盘等目标点位自身偏斜 的图型,进一步压制网格对齐类假阳性。 适用边界:原图背景暗像素占比不宜过高(如 >60%),且高频二值纹理 (噪声图、密集文字截图)会让深色命中统计饱和——两类画面均属点阵 通道拒绝域,应改走 DCT/LSB 通道溯源(见 dct.mbt)。

    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,避免全零状态死循环。