Swatches: swatch exchange (.ase) files open as swatch libraries

File › Open, Other Library… and the User Defined library folder read
.ase files. Colors keep their model, global and spot colors stay global
and spot, and color groups stay groups. The library readers take a
file's bytes. On the web, File › Open passes a swatch library file to
swatch.library.load as dataBase64 instead of as lossy UTF-8 text.

Swatch and group names in a library are now unique, in .gpl files too.
Add to Swatches puts each color group of a library into one document
group when the document already has a swatch with the group's name.
The library fuzz helpers fail when a command panics.
This commit is contained in:
dotCooCoo
2026-10-08 15:18:57 -07:00
parent c3b73f0146
commit e0b1f55250
14 changed files with 591 additions and 47 deletions
+61
View File
@@ -0,0 +1,61 @@
//! Hand-written swatch exchange (`.ase`) files for swatch library tests: version 1.0, big-endian,
//! with colors in each model and color type, and color groups.
/// One block of a swatch exchange file.
#[derive(Clone, Debug)]
pub enum Block<'a> {
/// A color group starts (its name).
GroupStart(&'a str),
/// The open color group ends.
GroupEnd,
/// A color: its name, model (`b"RGB "`, `b"CMYK"`, `b"LAB "` or `b"Gray"`), components and
/// color type (0 global, 1 spot, 2 process).
Color(&'a str, &'a [u8; 4], &'a [f32], u16),
}
/// A name: a `u16` count of UTF-16 code units, the terminating zero included, then the units.
fn name(out: &mut Vec<u8>, name: &str) {
let units: Vec<u16> = name.encode_utf16().chain([0]).collect();
out.extend(u16::try_from(units.len()).unwrap().to_be_bytes());
out.extend(units.iter().flat_map(|u| u.to_be_bytes()));
}
/// A swatch exchange file of `blocks`.
pub fn ase(blocks: &[Block]) -> Vec<u8> {
let mut out = b"ASEF\0\x01\0\0".to_vec();
out.extend(u32::try_from(blocks.len()).unwrap().to_be_bytes());
for b in blocks {
let mut body = vec![];
let kind: u16 = match b {
Block::GroupStart(n) => {
name(&mut body, n);
0xC001
}
Block::GroupEnd => 0xC002,
Block::Color(n, model, values, kind) => {
name(&mut body, n);
body.extend(*model);
body.extend(values.iter().flat_map(|v| v.to_be_bytes()));
body.extend(kind.to_be_bytes());
0x0001
}
};
out.extend(kind.to_be_bytes());
out.extend(u32::try_from(body.len()).unwrap().to_be_bytes());
out.extend(body);
}
out
}
/// A library of four colors: "Sky" (global RGB) and "Ink" (spot CMYK), then the group "Neutrals"
/// of the process colors "Mist" (Gray, 25% ink) and "Clay" (Lab 50, 20, −30).
pub fn sample() -> Vec<u8> {
ase(&[
Block::Color("Sky", b"RGB ", &[0.0, 0.5, 1.0], 0),
Block::Color("Ink", b"CMYK", &[1.0, 0.5, 0.0, 0.2], 1),
Block::GroupStart("Neutrals"),
Block::Color("Mist", b"Gray", &[0.75], 2),
Block::Color("Clay", b"LAB ", &[0.5, 20.0, -30.0], 2),
Block::GroupEnd,
])
}
+2
View File
@@ -10,6 +10,7 @@
//! - [`raster`]: rendering helpers and image comparison with a perceptual tolerance.
//! - [`geom`]: geometry assertions (approximate equality, curve sampling, Hausdorff distance).
//! - [`pdf`]: hand-written PDF files (page boxes, colour spaces, encryption) for import tests.
//! - [`ase`]: hand-written swatch exchange (`.ase`) files for swatch library tests.
//!
//! This crate may only be used as a dev-dependency (enforced by `cargo xtask layers`).
// Test support only (a dev-dependency of every crate that uses it): a failed setup or assertion
@@ -18,6 +19,7 @@
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![forbid(unsafe_code)]
pub mod ase;
pub mod fixtures;
pub mod geom;
pub mod invariants;