///|
/// 命令行工具:解析 ICC profile 并打印头部与 tag 表。
///
/// 用途有二:
/// 1. 给使用者一个可直接运行的示例;
/// 2. 作为外部 oracle 差分脚本的被测端——脚本用 Python 独立解析同一文件,
/// 两边的输出逐字段比对。
///
/// 用法:
/// `moon run cmd/main -- parse `
/// `moon run cmd/main -- transform `
///
/// 输出为稳定的纯文本格式,便于脚本解析。
///|
/// 把 tag 表渲染为每行一个条目的文本。
fn render_tags(entries : Array[@moon_icc.IccTagEntry]) -> String {
let buf = StringBuilder()
for e in entries {
buf.write("\{e.signature} \{e.offset} \{e.size}\n")
}
buf.to_string()
}
///|
/// 解析一个 profile 文件并打印结果。
fn describe(path : String) -> Unit raise {
let data = @fs.read_file_to_bytes(path)
let header = @moon_icc.parse_header(data)
println("size \{header.size}")
println("version \{header.major_version}.\{header.minor_version}")
println("class \{header.device_class}")
println("colorspace \{header.color_space}")
println("pcs \{header.pcs}")
println("datetime \{header.date_time.to_iso_string()}")
println("intent \{header.rendering_intent}")
println(
"illuminant \{header.illuminant.x} \{header.illuminant.y} \{header.illuminant.z}",
)
let entries = @moon_icc.parse_tag_table(data, header)
println("tagcount \{entries.length()}")
println(render_tags(entries))
}
///|
/// 对给定输入值做 matrix/TRC 变换并打印结果。
///
/// 输出格式:每行 `样本序号 输入 R G B X Y Z L a b`,
/// 便于外部 oracle 脚本逐行比对。
fn transform_samples(
profile : @moon_icc.Profile,
samples : Array[Double],
) -> Unit raise {
let t = profile.matrix_trc()
for v in samples {
let xyz = t.to_pcs(v, v, v)
let lab = @moon_icc.xyz_to_lab(xyz)
println("sample \{v} \{xyz.x} \{xyz.y} \{xyz.z} \{lab.l} \{lab.a} \{lab.b}")
}
}
///|
/// 读取 profile 并对固定样本点做变换。
///
/// 样本点刻意包含中间调(0.05..0.95),只有这样才能钉住
/// TRC 与矩阵的先后顺序——黑白点在任何顺序下都正确。
fn read_and_transform(path : String) -> Unit raise {
let data = @fs.read_file_to_bytes(path)
let profile = @moon_icc.Profile::parse(data)
let samples : Array[Double] = [
0.0, 0.05, 0.1, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8, 0.9, 0.95, 1.0,
]
transform_samples(profile, samples)
}
///|
/// 端到端变换:源 profile → 目标 profile,输出 8 位量化结果。
///
/// 输出格式:每行 `link <输入> <输出通道...>`,量化到 0..255,
/// 便于与 lcms2 之类的参考实现直接比对(参考实现通常在 8 位域上给结果)。
///
/// 输出通道数由**目标 profile** 决定:显示器 profile 是 3 个(RGB),
/// 打印机 profile 是 4 个(CMYK)。因此同一命令既能看 sRGB → 显示器,
/// 也能看 sRGB → CMYK。
///
/// 驱动样本是灰阶(输入三通道相同)。对 CMYK 目标而言这不是"灰度"——
/// 因为 CMYK 有四通道,灰阶只覆盖其中一个切片,但足以对比实现差异。
///
/// `intent` 用**意图名**而不是数字,避免在此处引入整数解析:
/// `perceptual` / `relative` / `saturation` / `absolute`。
fn read_and_link(
source_path : String,
dest_path : String,
intent : Int32,
) -> Unit raise {
let src_data = @fs.read_file_to_bytes(source_path)
let dst_data = @fs.read_file_to_bytes(dest_path)
let transform = @moon_icc.ColorTransform::with_intent(
@moon_icc.Profile::parse(src_data),
@moon_icc.Profile::parse(dst_data),
intent,
)
println("intent \{intent}")
println("src_channels \{transform.source_channels()}")
println("dst_channels \{transform.destination_channels()}")
for i = 0; i <= 255; i = i + 1 {
let v = i.to_double() / 255.0
let out = transform.transform_values([v, v, v])
let buf = StringBuilder()
for x in out {
buf.write(" \{quantise8(x)}")
}
println("link \{i}\{buf.to_string()}")
}
}
///|
/// 软打样:源设备 → 打样设备 → 显示设备。
///
/// 每行 `proof <索引> | <打样设备值> | <显示设备值>`,均量化到 0..255。
/// 前者说明「这张图印在目标设备上要用多少墨」,后者说明「屏幕上该显示成什么」。
fn read_and_proof(
source_path : String,
proof_path : String,
display_path : String,
intent : Int32,
proof_intent : Int32,
) -> Unit raise {
let chain = @moon_icc.ColorTransform::proofing(
@moon_icc.Profile::parse(@fs.read_file_to_bytes(source_path)),
@moon_icc.Profile::parse(@fs.read_file_to_bytes(proof_path)),
@moon_icc.Profile::parse(@fs.read_file_to_bytes(display_path)),
intent,
proof_intent,
)
println("intent \{intent}")
println("proof_intent \{proof_intent}")
println("proof_channels \{chain.proof_channels()}")
println("display_channels \{chain.display_channels()}")
for i = 0; i <= 255; i = i + 17 {
let v = i.to_double() / 255.0
let (proof_values, shown) = chain.evaluate([v, v, v])
let pb = StringBuilder()
for x in proof_values {
pb.write(" \{quantise8(x)}")
}
let sb = StringBuilder()
for x in shown {
sb.write(" \{quantise8(x)}")
}
println("proof \{i} |\{pb.to_string()} |\{sb.to_string()}")
}
}
///|
/// 色域判断:源 profile 的样本能否被目标 profile 表达。
///
/// 判据是**几何色域边界**(球面直方图,见 gamut.mbt),不是往返 ΔE:
/// 每行 `gamut <索引> R G B boundary= dist=<ΔE量级> mapping_de=<ΔE76>`。
/// 后两项刻意并列输出——`dist` 是到边界的距离,`mapping_de` 是该 profile
/// A2B/B2A 不互逆造成的映射误差,两者是不同的量。
fn read_and_gamut(
source_path : String,
target_path : String,
intent : Int32,
steps : Int32,
) -> Unit raise {
let source = @moon_icc.Profile::parse(@fs.read_file_to_bytes(source_path))
let target = @moon_icc.Profile::parse(@fs.read_file_to_bytes(target_path))
let boundary = target.gamut_boundary(intent, steps)
println("intent \{intent}")
println("sectors \{boundary.sectors()}")
println("samples \{boundary.samples()}")
println("coverage \{boundary.coverage()}")
let corners : Array[(Double, Double, Double)] = [
(0.0, 0.0, 0.0),
(1.0, 1.0, 1.0),
(1.0, 0.0, 0.0),
(0.0, 1.0, 0.0),
(0.0, 0.0, 1.0),
(0.0, 1.0, 1.0),
(1.0, 0.0, 1.0),
(1.0, 1.0, 0.0),
]
let mut index = 0
for c in corners {
report_gamut(index, source, target, boundary, intent, c.0, c.1, c.2)
index = index + 1
}
for i = 0; i <= 255; i = i + 17 {
let v = i.to_double() / 255.0
report_gamut(index, source, target, boundary, intent, v, v, v)
index = index + 1
}
}
///|
/// 单个样本的色域判定(供 `read_and_gamut` 调用)。
fn report_gamut(
index : Int32,
source : @moon_icc.Profile,
target : @moon_icc.Profile,
boundary : @moon_icc.GamutBoundary,
intent : Int32,
r : Double,
g : Double,
b : Double,
) -> Unit raise {
let xyz = source.device_to_pcs(intent, [r, g, b])
let lab = @moon_icc.xyz_to_lab(xyz)
let de = target.round_trip_delta_e(intent, xyz)
let dist = boundary.signed_distance(lab)
let verdict = if boundary.contains(lab) { "in" } else { "out" }
println(
"gamut \{index} \{quantise8(r)} \{quantise8(g)} \{quantise8(b)} boundary=\{verdict} dist=\{dist} mapping_de=\{de}",
)
}
///|
///|
/// 保留 4 位小数的定点格式(CLI 输出契约,供文档示例逐字引用)。
///
/// 全精度 Double 打印成 `54.00015259021897` 这种形态,无法写进 README 示例;
/// 这里用整数运算做四舍五入与补零,不依赖格式化库。
fn fmt4(x : Double) -> String {
let scaled = x * 10000.0
let truncated = scaled.to_int()
let rounded = if scaled >= 0.0 {
if scaled - truncated.to_double() >= 0.5 {
truncated + 1
} else {
truncated
}
} else if truncated.to_double() - scaled >= 0.5 {
truncated - 1
} else {
truncated
}
let neg = rounded < 0
let mag = if neg { -rounded } else { rounded }
let int_part = mag / 10000
let frac = mag % 10000
let pad = if frac < 10 {
"000"
} else if frac < 100 {
"00"
} else if frac < 1000 {
"0"
} else {
""
}
(if neg { "-" } else { "" }) +
int_part.to_string() +
"." +
pad +
frac.to_string()
}
///|
/// 把一条命名色的 PCS 统一成 Lab(L 0..100、a/b -128..127)。
///
/// **必须用该条自己的 `pcs()`,不能回查 `lookup_lab(full_name)`**:查表是首匹配
/// 语义,同根名的第 5 条会被显示成第 1 条的值(本项目真实踩到过)。
fn named_entry_lab(
table : @moon_icc.NamedColor2,
e : @moon_icc.NamedColorEntry,
) -> (Double, Double, Double) {
let pcs = e.pcs()
if table.pcs_space() == "Lab " {
(pcs.x, pcs.y, pcs.z)
} else if table.pcs_space() == "XYZ " {
let lab = @moon_icc.xyz_to_lab(pcs)
(lab.l, lab.a, lab.b)
} else {
(pcs.x, pcs.y, pcs.z)
}
}
///|
/// 命名色:打印 `ncl2` 的字段与「色名 → PCS」查表结果。
///
/// 输出契约固定(供脚本与文档示例消费):先若干行 `字段 值`,再每行
/// `named <序号> <完整色名> lab= device=<坐标...>`,
/// 最后两行演示「完整色名命中、裸根名不命中」。
fn describe_named(path : String) -> Unit raise {
let profile = @moon_icc.Profile::parse(@fs.read_file_to_bytes(path))
let table = profile.named_colors()
println("class \{profile.header.device_class}")
println("pcs \{table.pcs_space()}")
println("version \{table.major_version()}")
println("vendor_flag \{table.vendor_flag()}")
println("n_device_coords \{table.device_coords_count()}")
println("prefix \{table.prefix()}")
println("suffix \{table.suffix()}")
println("count \{table.length()}")
for i = 0; i < table.length(); i = i + 1 {
match table.entry_at(i) {
None => ()
Some(e) => {
let coords = e.device_coords()
let buf = StringBuilder()
for k = 0; k < coords.length(); k = k + 1 {
if k > 0 {
buf.write(" ")
}
buf.write("\{coords[k]}")
}
let lab = named_entry_lab(table, e)
println(
"named \{i} \{e.full_name()} lab=\{fmt4(lab.0)} \{fmt4(lab.1)} \{fmt4(lab.2)} device=\{buf.to_string()}",
)
}
}
}
// 查表语义演示:完整色名(prefix+根名+suffix)命中,裸根名不命中。
let first = match table.entry_at(0) {
Some(e) => e
None => return
}
let hit = match table.lookup_lab(first.full_name()) {
Some(v) => "\{fmt4(v.0)} \{fmt4(v.1)} \{fmt4(v.2)}"
None => "未命中"
}
println("lookup \{first.full_name()} -> \{hit}")
let bare = match table.lookup_lab(first.root_name()) {
Some(_) => "命中(不应发生)"
None => "未命中"
}
println("lookup \{first.root_name()} -> \{bare}")
}
///|
/// 一致性检查:打印目标规范版本、覆盖矩阵,并(给了路径时)检查一个 profile。
///
/// 这是把「标准版本与必需行为的覆盖目标」落成**可执行**的形式:
/// 每个 profile 类的必需 tag 会被实际检查,缺失项直接列出。
fn read_and_conformance(path : String?) -> Unit raise {
println("target_spec \{@moon_icc.target_specification}")
match path {
Some(p) => {
let profile = @moon_icc.Profile::parse(@fs.read_file_to_bytes(p))
let report = profile.conformance()
println("class \{report.device_class()}")
println("version \{report.version()}")
println("transform_supported \{report.transform_supported()}")
println("missing_required_tags \{report.missing().length()}")
for m in report.missing() {
println(" missing_tag \{m}")
}
println("note \{report.note()}")
}
None => ()
}
println("--- 设备类覆盖矩阵 ---")
for row in @moon_icc.conformance_matrix() {
println("\{row.0} \{row.1} \{row.2}")
}
}
///|
/// 把意图名解析为编号;无法识别时返回 `None`(不静默回退)。
fn parse_intent(name : String) -> Int32? {
match name {
"perceptual" => Some(0)
"relative" => Some(1)
"saturation" => Some(2)
"absolute" => Some(3)
_ => None
}
}
///|
/// 取可选位置的意图参数:缺省返回 `default`,给了但无法识别则抛错。
fn parse_optional_intent(
args : Array[String],
index : Int32,
default : Int32,
) -> Int32? {
if index >= args.length() {
return Some(default)
}
parse_intent(args[index])
}
///|
/// 取可选意图并在无法识别时打印提示;返回 `None` 表示应中止。
fn required_intent(
args : Array[String],
index : Int32,
default : Int32,
what : String,
) -> Int32? {
match parse_optional_intent(args, index, default) {
Some(v) => Some(v)
None => {
println(
"无法识别的\{what} \"\{args[index]}\"(可选: perceptual / relative / saturation / absolute)",
)
None
}
}
}
///|
/// 把 0..1 的值四舍五入到 0..255 的整数(参考实现通常在 8 位域上给结果)。
fn quantise8(x : Double) -> Int32 {
let scaled = x * 255.0
let rounded = if scaled - scaled.to_int().to_double() >= 0.5 {
scaled.to_int() + 1
} else {
scaled.to_int()
}
if rounded < 0 {
0
} else if rounded > 255 {
255
} else {
rounded
}
}
///|
/// 打印 LUT 结构,并做一次 CMYK 的 K 轴扫描(CMYK 打印机 profile 用)。
///
/// 输出格式:先若干行 `a2b <字段> <值>` 描述 LUT,再每行
/// `k <输入> ` 给出 CMYK=(0,0,0,k) 的 PCS Lab。
fn describe_lut(path : String) -> Unit raise {
let profile = @moon_icc.Profile::parse(@fs.read_file_to_bytes(path))
println("class \{profile.header.device_class}")
println("colorspace \{profile.header.color_space}")
println("pcs \{profile.header.pcs}")
println(
"version \{profile.header.major_version}.\{profile.header.minor_version}",
)
let table = profile.a2b(1)
println("a2b_tag \{@moon_icc.intent_to_a2b_tag(1)}")
println("a2b_type \{table.type_name()}")
println("a2b_input_channels \{table.input_channels()}")
println("a2b_output_channels \{table.output_channels()}")
println("a2b_grid_points \{table.grid_text()}")
println("a2b_bit_depth \{table.bit_depth()}")
// PCS → 设备方向的结构(供外部 oracle 差分比对,见 tools/diff_oracle.py)
let back = profile.b2a(1)
println("b2a_tag \{@moon_icc.intent_to_b2a_tag(1)}")
println("b2a_type \{back.type_name()}")
println("b2a_input_channels \{back.input_channels()}")
println("b2a_output_channels \{back.output_channels()}")
println("b2a_grid_points \{back.grid_text()}")
println("b2a_bit_depth \{back.bit_depth()}")
// K 轴扫描:纯黑墨从 0 到 100%
for i = 0; i <= 255; i = i + 17 {
let v = i.to_double() / 255.0
let lab = profile.device_to_lab(1, [0.0, 0.0, 0.0, v])
println("k \{i} \{lab.l} \{lab.a} \{lab.b}")
}
}
///|
/// 入口:
/// `moon run cmd/main -- parse `
/// `moon run cmd/main -- transform `
/// `moon run cmd/main -- link <源 profile> <目标 profile> [意图]`
/// `moon run cmd/main -- lut `
/// `moon run cmd/main -- proof <源> <打样> <显示> [意图] [打样意图]`
/// `moon run cmd/main -- gamut <源 profile> <目标 profile> [意图]`
///
/// 参数按关键词扫描而非固定下标:不同后端下 `moon run` 传给程序的
/// argv 形状不同(js 后端会把脚本路径放进 argv),
/// 扫描关键词在两种后端下都成立。
fn main {
let args = @env.args()
let command_index = args.search_by(fn(a) {
a == "parse" ||
a == "transform" ||
a == "link" ||
a == "lut" ||
a == "proof" ||
a == "gamut" ||
a == "named" ||
a == "conformance"
})
match command_index {
None => {
println("用法:")
println(" moon run cmd/main -- parse ")
println(" moon run cmd/main -- transform ")
println(
" moon run cmd/main -- link <源 profile> <目标 profile> [意图]",
)
println(" moon run cmd/main -- lut ")
println(" moon run cmd/main -- named ")
println(" moon run cmd/main -- conformance [profile 路径]")
println(
" moon run cmd/main -- proof <源> <打样> <显示> [意图] [打样意图]",
)
println(
" moon run cmd/main -- gamut <源 profile> <目标 profile> [意图]",
)
println("")
println("意图可选(默认 relative):")
println(" perceptual | relative | saturation | absolute")
}
Some(i) => {
let command = args[i]
// 各命令需要的 profile 路径数不同
let needed_paths = match command {
"proof" => 3
"link" | "gamut" => 2
"conformance" => 0
_ => 1
}
if i + needed_paths >= args.length() {
println("缺少 profile 路径参数")
return
}
match command {
"parse" =>
describe(args[i + 1]) catch {
e => println("解析失败: \{e}")
}
"transform" =>
read_and_transform(args[i + 1]) catch {
e => println("变换失败: \{e}")
}
"link" => {
// 意图是可选的第 3 个参数;给了但无法识别则报错,不静默回退
let intent_arg = if i + 3 < args.length() {
Some(args[i + 3])
} else {
None
}
let intent = match intent_arg {
None => 1
Some(name) =>
match parse_intent(name) {
Some(v) => v
None => {
println(
"无法识别的意图 \"\{name}\"(可选: perceptual / relative / saturation / absolute)",
)
return
}
}
}
read_and_link(args[i + 1], args[i + 2], intent) catch {
e => println("链接失败: \{e}")
}
}
"lut" =>
describe_lut(args[i + 1]) catch {
e => println("LUT 求值失败: \{e}")
}
"proof" => {
let intent = match required_intent(args, i + 4, 1, "意图") {
Some(v) => v
None => return
}
let proof_intent = match
required_intent(args, i + 5, 1, "打样意图") {
Some(v) => v
None => return
}
read_and_proof(
args[i + 1],
args[i + 2],
args[i + 3],
intent,
proof_intent,
) catch {
e => println("打样失败: \{e}")
}
}
"named" =>
describe_named(args[i + 1]) catch {
e => println("命名色读取失败: \{e}")
}
"conformance" => {
let path = if i + 1 < args.length() {
Some(args[i + 1])
} else {
None
}
read_and_conformance(path) catch {
e => println("一致性检查失败: \{e}")
}
}
"gamut" => {
let intent = match required_intent(args, i + 3, 1, "意图") {
Some(v) => v
None => return
}
// 采样层数是可选的第 4 个参数(默认 9:CMYK 得 6561 个样本)
let steps = if i + 4 < args.length() {
match @moon_icc.parse_steps(args[i + 4]) {
Some(v) => v
None => {
println(
"采样层数应为 2..12 的整数,实际 \"\{args[i + 4]}\"",
)
return
}
}
} else {
9
}
read_and_gamut(args[i + 1], args[i + 2], intent, steps) catch {
e => println("色域判断失败: \{e}")
}
}
_ => println("未知命令: \{command}")
}
}
}
}