PNG encoder/decoder written in MoonBit
基于 MoonBit 的 PNG 编解码库。 纯 MoonBit 实现;唯一第三方依赖是已实测过的 moonbit-community/flate(压缩层)。
| 能力 | 说明 |
|---|---|
| 解码 Identity | 任意合法位深 / 颜色类型,含 Adam7 隔行;输出文件里写的通道形态与打包字节 |
| 解码 Expand | 调色板 / 1·2·4bit / tRNS 合成 → 恒 8bit RGBA;16bit 取高字节 |
| 编码 Identity | decode 的结果原样写回;五种行滤波 + 自适应选型 |
| 两条压缩后端 | Flate(默认,真实压缩)/ Stored(逐字节等于 CPython compressobj(0)) |
| 容器写出 | PLTE / tRNS(调色板 alpha 表、灰度 / 真彩指定透明色)、Adam7 七趟编码 |
| 错误面 | 解码 6 类 + 编码 7 类,按「调用方该做什么」分类,tag() 是稳定契约 |
curl -fsSL https://cli.moonbitlang.cn/install/unix.sh | bash
export PATH="$HOME/.moon/bin:$PATH"moon add TryAndRun-TvT/png-moon# 在你的 moon.pkg 里引用:库在 lib/ 包,模块根是空包(README 文档测试宿主)
import {
"TryAndRun-TvT/png-moon/lib" @png,
}// 解码:Identity 拿到原始位深 / 通道;Expand 拿到 8bit RGBA
match decode(png) {
Ok(img) => img.pixels // width / height / color / depth / pixels
Err(e) => e.tag() // "bad_chunk:crc_mismatch" / "truncated" / "limit:pixels" …
}
// 编码:默认 Flate;要逐字节可复算就显式 Stored
match encode(img) { Ok(bytes) => .. Err(e) => e.tag() .. }
match encode(img, options=EncodeOptions::new(compression=Stored)) { .. }
// 调色板图必须显式给 PLTE:Identity 口径下 decode 不返回调色板
match encode(img, options=EncodeOptions::new(palette=Some(plte), trns=Some(alpha))) { .. }///|
test "解码一张 1x1 RGBA8 PNG" {
let png : Bytes = b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0dIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0dIDATx\xda\x63\xf8\xcf\xc0\xf0\x1f\x00\x05\x00\x01\xff\x56\xc7\x2f\x0d\x00\x00\x00\x00IEND\xae\x42\x60\x82"
// 文档测试宿主(模块根包)不 import 库,故这里只验字节层面的不变量;
// 真正跑 decode 的用例在 lib/decode_test.mbt(黑盒,黄金值来自 Pillow/CPython)。
inspect(png.length(), content="70")
inspect(png[0], content="b'\\x89'")
inspect(png[12], content="b'\\x49'")
}
///|
test "编解码调用形态(伪代码,真用例见 lib/decode_test.mbt / lib/expand_test.mbt / lib/encode_test.mbt)" {
// 解出来的 DecodedImage 是:宽 / 高 / 颜色类型 / 位深 / 紧凑像素字节
// match @lib.decode(png, options? = DecodeOptions::new(mode=Identity)) {
// Ok(img) => img.pixels // 行优先、无 filter 字节;Identity 下是原始打包字节
// Err(e) => e.tag() // 稳定标签,如 "bad_chunk:crc_mismatch"
// }
//
// DecodeOptions::new(mode=Expand) 的契约:输出恒为 8bit RGBA
// (调色板查 PLTE,tRNS 合成 alpha,低位深按 v*255/(2^n-1) 展开)。
//
// 编回去(Identity 口径进、Identity 口径出):
// match @lib.encode(img) {
// Ok(png) => png // signature + IHDR + [PLTE] + [tRNS] + IDAT + IEND
// // IDAT 默认是 flate 真实压缩;要逐字节可复算就传 Stored
// Err(e) => e.tag() // "bad_dimensions" / "bad_pixel_length" /
// // "bad_palette:*" / "bad_trns:*" / "bad_pixel_value"
// }
//
// 调色板图要显式给 PLTE(Identity 口径下 decode 拿不到它):
// EncodeOptions::new(palette=Some(plte), trns=Some(alpha_table))
// 要「产物可复算」(指纹 / 金丝雀 / 逐字节对照):EncodeOptions::new(compression=Stored)
// 渐进式图:EncodeOptions::new(interlace=Adam7)(默认仍是 None,且不打算换)
inspect(1, content="1")
}这个 mbt check 块会被 moon test 真编译、真运行(README.mbt.md 是符号链接)。 它只用内置类型——文档测试宿主是模块根的空包,不 import 本库。
moon build lib # 编译库包
moon build cmd/main --release # CLI(默认 wasm-gc)
moon build cmd/main --target wasm --release # wasm MVP 后端
moon run cmd/main # CLI 自检(Identity + Expand + Adam7 + 往返)
moon test # 单元 / 文档测试
bash scripts/gates.sh # 一键门禁(本地唯一闸门)
bash scripts/wasm-backend.sh # 第二后端轴:wasm 的 check/test/自检 + 外部真值
bash scripts/release-check.sh # 发布前:归档面 / 干净环境 / 归档内真实编解码| 文档 | 内容 |
|---|---|
| 项目总览 | 现状、API 契约、边界、七层真值、门禁、复跑命令、演进史 |
| 规格与移植方案 | 目标与非目标、分层与文件职责、上游映射、生态选型 |
| 执行方案与验收真值 | 任务包、chunk 顺序约束、里程碑 U/V、L1–L7 真值细节 |
| 工程约定与配置台账 | 配置来源、门禁阶段、工具链、逐文件处置 |
| 解码与编码落地实测 | 探针、flate 接入、Identity、Expand、编码 / 容器 / Adam7 |
| 压缩性能与尺子实测 | 真实压缩、体积 / 耗时、默认值换挡、尺子的重建 |
| 验收完备性与发布实测 | 往返四关、坏图生成器、16bit、完成度复核、wasm 后端 |
.
├── moon.mod # MoonBit 模块配置
├── moon.pkg # 模块根空包:README 文档测试宿主(不写库代码)
├── lib/ # 库包(公共 API 外壳;实现细节见 lib/internal/)
├── cmd/main/ # 可执行包:CLI 自检
├── cmd/{conformance,encode,roundtrip,bench}/ # 门禁宿主(不进发布归档)
├── probe/ # 可行性探针(证据面,非库代码)
├── docs/ # 文档(入口 = docs/README.md)
├── scripts/ # 构建 / 门禁脚本
├── AGENTS.md # AI 协作代理指南(贡献前必读)
├── README.mbt.md -> README.md # 文档测试入口(符号链接)
└── LICENSE # Apache-2.0