///|
/// 命名色(`ncl2` / namedColor2Type)的解析与查询。
///
/// 依据 ICC.1:2010-12(ISO 15076-1:2010)§10.14(namedColor2Type),以及
/// 表 17–23 中 `nmcl` 设备类对 `ncl2` 的必需要求。
///
/// tag 数据布局(大端序,偏移相对 tag 数据起始):
///
/// ```
/// 0 4 'ncl2' 类型签名
/// 4 4 保留位(规范要求为 0)
/// 8 4 vendorFlag 厂商自定义标志
/// 12 4 count 命名色条数
/// 16 4 nDeviceCoords 每条附带的设备坐标个数(0..8)
/// 20 32 prefix 色名前缀(ASCII,尾部 0x00 填充)
/// 52 32 suffix 色名后缀(ASCII,尾部 0x00 填充)
/// 84 .. count 个条目,每个 `38 + 2*nDeviceCoords` 字节:
/// 32 字节 rootName(ASCII,尾部 0x00 填充)
/// 3 × uInt16Number PCS 坐标
/// nDeviceCoords × u16 设备坐标
/// ```
///
/// **PCS 坐标是 3 个 `uInt16Number`,不是 `s15Fixed16Number`**:与 LUT 里的
/// PCS 值同一种编码(Lab:L 0..100 ↔ 0..0xFFFF、a/b -128..127 ↔ 0..0xFFFF,
/// v2 的 16 位上限是 `0xFF00`;XYZ:`u1Fixed15Number`,1.0 ↔ `0x8000`)。
/// 本实现第一版按 `s15Fixed16Number` 写过一次,被 lcms2 的 `cmsNamedColorInfo`
/// 当场否掉(第一条正常、第二条起全部错位)——这条注释是实测结论,不是推断。
///
/// **完整色名 = prefix + rootName + suffix**:规范对色名的定义如此。查表用完整
/// 色名才能区分同根名、不同后缀的条目(`PANTONE Red C` 与 `PANTONE Red U`)。
///|
/// `ncl2` 固定头部字节数:签名 4 + 保留 4 + vendorFlag 4 + count 4 +
/// nDeviceCoords 4 + prefix 32 + suffix 32。
let named_color2_header_size : Int32 = 84
///|
/// 每条命名色的固定字节数:根名 32 + PCS 3×u16(6 字节)。
let named_color2_entry_base : Int32 = 38
///|
/// 规范允许的最大设备坐标个数(ICC.1:2010-12 §10.14)。
let named_color2_max_device_coords : Int32 = 8
///|
/// 一条命名色记录。
///
/// `pcs` 已按 profile 的 PCS 字段与版本解码:PCS 为 `Lab ` 时是
/// L(0..100)/a(-128..127)/b(-128..127),为 `XYZ ` 时是 XYZ 三刺激值。
pub struct NamedColorEntry {
/// tag 里的 32 字节根名(已去尾部 0x00)。
root_name : String
/// 完整色名:`prefix + root_name + suffix`。
full_name : String
/// 解码后的 PCS 三分量。
pcs : XyzNumber
/// 附带的设备坐标(个数由 `nDeviceCoords` 决定,可为空)。
device_coords : Array[Int32]
}
///|
/// 解析后的 `ncl2` tag。
pub struct NamedColor2 {
/// 厂商自定义标志(`vendorFlag`)。
vendor_flag : Int32
/// 色名前缀(已去尾部 0x00)。
prefix : String
/// 色名后缀(已去尾部 0x00)。
suffix : String
/// 每条附带的设备坐标个数(`nDeviceCoords`)。
device_coords_count : Int32
/// profile 头部声明的 PCS(`Lab ` / `XYZ `);空串表示解析时未提供。
pcs_space : String
/// profile 头部的主版本号(决定 Lab 的 16 位上限:v2 用 0xFF00)。
major_version : Int32
/// 命名色条目,顺序与文件中一致。
colors : Array[NamedColorEntry]
}
///|
/// 读取固定长度的 ASCII 名字段(`prefix`/`suffix`/`rootName`)。
///
/// 规范约定这些字段以 0x00 填充到固定长度。这里**无论是否遇到 0x00 都要消费
/// 满 `n` 字节**(否则后续字段全部错位),只把第一个 0x00 之前的内容当作名字。
/// 与 `read_signature` 一致,可打印 ASCII 之外的字节渲染成 `.`,避免畸形输入
/// 里的控制字符进入日志。
fn read_fixed_ascii(r : BigEndianReader, n : Int32) -> String raise IccError {
let buf = StringBuilder()
let mut terminated = false
for i = 0; i < n; i = i + 1 {
let b = r.read_u8()
if b == 0 {
terminated = true
} else if !terminated {
if b >= 32 && b < 127 {
buf.write_char(b.unsafe_to_char())
} else {
buf.write_char('.')
}
}
}
buf.to_string()
}
///|
/// 把 tag 里的 3 个 `uInt16Number` 解码为 PCS 三分量。
///
/// - `Lab `:v4 上限 `0xFFFF`、v2 上限 `0xFF00`;
/// - `XYZ `:`u1Fixed15Number`,1.0 ↔ `0x8000`;
/// - 其他 PCS:原样保留 u16 数值(不猜空间),此时 `lookup_lab` 返回 `None`。
fn decode_named_pcs(
raw0 : Int32,
raw1 : Int32,
raw2 : Int32,
pcs_space : String,
major_version : Int32,
) -> XyzNumber {
match pcs_space {
"Lab " => {
let scale = if major_version < 4 { 65280.0 } else { 65535.0 }
XyzNumber::{
x: raw0.to_double() / scale * 100.0,
y: raw1.to_double() / scale * 255.0 - 128.0,
z: raw2.to_double() / scale * 255.0 - 128.0,
}
}
"XYZ " =>
XyzNumber::{
x: raw0.to_double() / 32768.0,
y: raw1.to_double() / 32768.0,
z: raw2.to_double() / 32768.0,
}
_ =>
XyzNumber::{
x: raw0.to_double(),
y: raw1.to_double(),
z: raw2.to_double(),
}
}
}
///|
/// 解析 `ncl2` tag,并记录 profile 的 PCS 字段与主版本(决定 Lab 解码口径)。
fn parse_named_color2_as(
data : Bytes,
entry : IccTagEntry,
pcs_space : String,
major_version : Int32,
) -> NamedColor2 raise IccError {
let (type_sig, body_at) = tag_type_and_data(data, entry)
if type_sig != "ncl2" {
raise BadTagData(
tag=entry.signature,
reason="期望类型 \"ncl2\",实际 \"\{type_sig}\"",
)
}
if entry.size < named_color2_header_size {
raise BadTagData(
tag=entry.signature,
reason="tag 只有 \{entry.size} 字节,不足以容纳 \{named_color2_header_size} 字节头部",
)
}
let r = BigEndianReader::new(data)
r.seek(body_at)
let vendor_flag = r.read_u32()
let count = r.read_u32()
let n_coords = r.read_u32()
if n_coords > named_color2_max_device_coords {
raise BadTagData(
tag=entry.signature,
reason="nDeviceCoords = \{n_coords} 超出规范允许的 0..\{named_color2_max_device_coords}",
)
}
let prefix = read_fixed_ascii(r, 32)
let suffix = read_fixed_ascii(r, 32)
// 条目必须整体落在本 tag 声明的范围内:用除法而不是乘法做上界判断,
// 避免 count * entry_size 在 Int32 上溢出后绕过检查。
let tag_end = entry.offset + entry.size
let available = tag_end - r.position()
let entry_size = named_color2_entry_base + 2 * n_coords
if available < 0 || count > available / entry_size {
raise BadTagData(
tag=entry.signature,
reason="声明 \{count} 条命名色(每条 \{entry_size} 字节),但 tag 内只剩 \{available} 字节",
)
}
let colors : Array[NamedColorEntry] = []
for i = 0; i < count; i = i + 1 {
let root_name = read_fixed_ascii(r, 32)
let raw0 = r.read_u16()
let raw1 = r.read_u16()
let raw2 = r.read_u16()
let pcs = decode_named_pcs(raw0, raw1, raw2, pcs_space, major_version)
let device_coords : Array[Int32] = []
for k = 0; k < n_coords; k = k + 1 {
device_coords.push(r.read_u16())
}
colors.push({
root_name,
full_name: prefix + root_name + suffix,
pcs,
device_coords,
})
}
{
vendor_flag,
prefix,
suffix,
device_coords_count: n_coords,
pcs_space,
major_version,
colors,
}
}
///|
/// 解析 `ncl2` tag(不提供 profile 的 PCS 与版本:PCS 记为空串、按 v4 解码)。
///
/// 一般通过 `Profile::named_colors` 调用,那里会带上头部的 PCS 与主版本,
/// 使 `lookup_lab` 能把 PCS 正确解释成 Lab。
pub fn parse_named_color2(
data : Bytes,
entry : IccTagEntry,
) -> NamedColor2 raise IccError {
parse_named_color2_as(data, entry, "", 4)
}
///|
/// 读取该 profile 的命名色表(`ncl2` tag)。
///
/// 命名色 profile(设备类 `nmcl`)的必需行为就是这张表:色名 → PCS 查表。
/// 缺少 `ncl2` 的 profile 会报 `BadTagData`,不静默返回空表。
pub fn Profile::named_colors(self : Profile) -> NamedColor2 raise IccError {
match self.find_tag("ncl2") {
Some(t) =>
parse_named_color2_as(
self.data,
t,
self.header.pcs,
self.header.major_version,
)
None =>
raise BadTagData(
tag="ncl2",
reason="profile 中没有 ncl2 tag(不是命名色 profile)",
)
}
}
///|
/// 命名色条数。
pub fn NamedColor2::length(self : NamedColor2) -> Int32 {
self.colors.length()
}
///|
/// 色名前缀(`prefix`)。
pub fn NamedColor2::prefix(self : NamedColor2) -> String {
self.prefix
}
///|
/// 色名后缀(`suffix`)。
pub fn NamedColor2::suffix(self : NamedColor2) -> String {
self.suffix
}
///|
/// 厂商自定义标志(`vendorFlag`)。
pub fn NamedColor2::vendor_flag(self : NamedColor2) -> Int32 {
self.vendor_flag
}
///|
/// 每条命名色附带的设备坐标个数(`nDeviceCoords`)。
pub fn NamedColor2::device_coords_count(self : NamedColor2) -> Int32 {
self.device_coords_count
}
///|
/// profile 头部声明的 PCS(`Lab ` / `XYZ `;空串表示解析时未提供)。
pub fn NamedColor2::pcs_space(self : NamedColor2) -> String {
self.pcs_space
}
///|
/// 解码时使用的主版本号(决定 Lab 的 16 位上限)。
pub fn NamedColor2::major_version(self : NamedColor2) -> Int32 {
self.major_version
}
///|
/// 按序号取条目;越界返回 `None`。
pub fn NamedColor2::entry_at(
self : NamedColor2,
index : Int32,
) -> NamedColorEntry? {
if index < 0 || index >= self.colors.length() {
return None
}
Some(self.colors[index])
}
///|
/// 按**完整色名**(`prefix + 根名 + suffix`)查表,返回第一条匹配项。
///
/// 匹配是 ASCII 精确比较(区分大小写):`ncl2` 的色名按规范是 ASCII 且以固定
/// 长度存储,规范未定义大小写折叠规则,因此这里不做折叠,避免自造语义。
/// 同根名重复出现时取文件中靠前的一条(与线性查表语义一致)。
pub fn NamedColor2::lookup(
self : NamedColor2,
full_name : String,
) -> NamedColorEntry? {
for c in self.colors {
if c.full_name == full_name {
return Some(c)
}
}
None
}
///|
/// 按完整色名查表并返回 PCS 的 **Lab** 表示(L 0..100、a/b -128..127)。
///
/// PCS 为 `Lab ` 时原样返回;为 `XYZ ` 时经 `xyz_to_lab` 转换。
/// `pcs_space` 未知(空串)或既非 Lab 也非 XYZ 时返回 `None`——
/// 不猜一个空间出来,避免给出错误颜色。
pub fn NamedColor2::lookup_lab(
self : NamedColor2,
full_name : String,
) -> (Double, Double, Double)? {
match self.lookup(full_name) {
None => None
Some(e) =>
if self.pcs_space == "Lab " {
Some((e.pcs.x, e.pcs.y, e.pcs.z))
} else if self.pcs_space == "XYZ " {
let lab = xyz_to_lab(e.pcs)
Some((lab.l, lab.a, lab.b))
} else {
None
}
}
}
///|
/// 根名(tag 里的 32 字节名字段,不含前缀与后缀)。
pub fn NamedColorEntry::root_name(self : NamedColorEntry) -> String {
self.root_name
}
///|
/// 完整色名:`prefix + root_name + suffix`。
pub fn NamedColorEntry::full_name(self : NamedColorEntry) -> String {
self.full_name
}
///|
/// 解码后的 PCS 三分量。
pub fn NamedColorEntry::pcs(self : NamedColorEntry) -> XyzNumber {
self.pcs
}
///|
/// 附带的设备坐标(可能为空)。
pub fn NamedColorEntry::device_coords(self : NamedColorEntry) -> Array[Int32] {
self.device_coords
}