use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use agg_gui::color::Color;
use agg_gui::draw_ctx::FillRule;
use agg_gui::lcd_coverage::{composite_lcd_mask, LcdBuffer, LcdMask, LcdMaskBuilder};
use agg_rust::basics::PATH_FLAGS_NONE;
use agg_rust::path_storage::PathStorage;
use agg_rust::trans_affine::TransAffine;
const COS30: f64 = 0.8660254037844387;
const SIN30: f64 = 0.5;
const KAPPA: f64 = 0.5522847498307933;
const CARGO_LOCK: &str = include_str!("../../Cargo.lock");
#[derive(Clone, Copy)]
enum Cmd {
MoveTo(f64, f64),
LineTo(f64, f64),
Curve4(f64, f64, f64, f64, f64, f64),
Close,
}
#[derive(Clone, Copy)]
enum DstFill {
Solid([u8; 4]),
Halves(i32, [u8; 4], [u8; 4]),
}
#[derive(Clone, Copy)]
struct CompositeCase {
dst_w: u32,
dst_h: u32,
fill: DstFill,
src: [u8; 4],
origin: (i32, i32),
}
#[derive(Clone, Copy)]
struct BufferCase {
buf_w: u32,
buf_h: u32,
clear: [u8; 4],
src: [u8; 4],
}
struct Case {
name: &'static str,
mask_w: u32,
mask_h: u32,
fill_rule: FillRule,
clip: Option<(f64, f64, f64, f64)>,
xform: TransAffine,
paths: Vec<Vec<Cmd>>,
composite: Option<CompositeCase>,
buffer: Option<BufferCase>,
}
fn identity() -> TransAffine {
TransAffine {
sx: 1.0,
shy: 0.0,
shx: 0.0,
sy: 1.0,
tx: 0.0,
ty: 0.0,
}
}
fn translation(tx: f64, ty: f64) -> TransAffine {
TransAffine {
sx: 1.0,
shy: 0.0,
shx: 0.0,
sy: 1.0,
tx,
ty,
}
}
fn scale_translate(scale: f64, tx: f64, ty: f64) -> TransAffine {
TransAffine {
sx: scale,
shy: 0.0,
shx: 0.0,
sy: scale,
tx,
ty,
}
}
fn rot30_scale_translate(scale: f64, tx: f64, ty: f64) -> TransAffine {
TransAffine {
sx: COS30 * scale,
shy: SIN30 * scale,
shx: -SIN30 * scale,
sy: COS30 * scale,
tx,
ty,
}
}
fn rect(x1: f64, y1: f64, x2: f64, y2: f64) -> Vec<Cmd> {
vec![
Cmd::MoveTo(x1, y1),
Cmd::LineTo(x2, y1),
Cmd::LineTo(x2, y2),
Cmd::LineTo(x1, y2),
Cmd::Close,
]
}
fn star() -> Vec<Cmd> {
vec![
Cmd::MoveTo(20.0, 35.5),
Cmd::LineTo(26.25, 12.5),
Cmd::LineTo(4.75, 26.75),
Cmd::LineTo(35.25, 26.75),
Cmd::LineTo(13.75, 12.5),
Cmd::Close,
]
}
fn circle(cx: f64, cy: f64, r: f64) -> Vec<Cmd> {
let k = r * KAPPA;
vec![
Cmd::MoveTo(cx + r, cy),
Cmd::Curve4(cx + r, cy + k, cx + k, cy + r, cx, cy + r),
Cmd::Curve4(cx - k, cy + r, cx - r, cy + k, cx - r, cy),
Cmd::Curve4(cx - r, cy - k, cx - k, cy - r, cx, cy - r),
Cmd::Curve4(cx + k, cy - r, cx + r, cy - k, cx + r, cy),
Cmd::Close,
]
}
fn cases() -> Vec<Case> {
let composite_over_white = CompositeCase {
dst_w: 48,
dst_h: 30,
fill: DstFill::Solid([255, 255, 255, 255]),
src: [0, 0, 0, 255],
origin: (4, 3),
};
let composite_light_on_dark = CompositeCase {
dst_w: 44,
dst_h: 28,
fill: DstFill::Halves(22, [32, 32, 40, 255], [200, 60, 10, 255]),
src: [255, 255, 255, 128],
origin: (-3, -2),
};
let buffer_over_translucent_slate = BufferCase {
buf_w: 40,
buf_h: 40,
clear: [40, 60, 90, 128],
src: [230, 220, 255, 200],
};
vec![
Case {
name: "rect_frac_identity",
mask_w: 48,
mask_h: 30,
fill_rule: FillRule::NonZero,
clip: None,
xform: identity(),
paths: vec![rect(5.25, 4.75, 28.5, 16.25)],
composite: Some(composite_over_white),
buffer: None,
},
Case {
name: "rect_frac_translate",
mask_w: 48,
mask_h: 30,
fill_rule: FillRule::NonZero,
clip: None,
xform: translation(0.25, 0.75),
paths: vec![rect(5.25, 4.75, 28.5, 16.25)],
composite: None,
buffer: None,
},
Case {
name: "rect_rot30",
mask_w: 48,
mask_h: 40,
fill_rule: FillRule::NonZero,
clip: None,
xform: rot30_scale_translate(1.0, 24.0, 20.0),
paths: vec![rect(-8.0, -5.0, 8.0, 5.0)],
composite: None,
buffer: None,
},
Case {
name: "rect_scale375",
mask_w: 48,
mask_h: 36,
fill_rule: FillRule::NonZero,
clip: None,
xform: scale_translate(3.75, 2.0, 2.0),
paths: vec![rect(1.25, 1.5, 9.75, 7.25)],
composite: None,
buffer: None,
},
Case {
name: "sliver_triangle",
mask_w: 24,
mask_h: 28,
fill_rule: FillRule::NonZero,
clip: None,
xform: identity(),
paths: vec![vec![
Cmd::MoveTo(6.5, 3.25),
Cmd::LineTo(7.125, 24.5),
Cmd::LineTo(8.25, 3.25),
Cmd::Close,
]],
composite: None,
buffer: None,
},
Case {
name: "sliver_triangle_rot30",
mask_w: 32,
mask_h: 32,
fill_rule: FillRule::NonZero,
clip: None,
xform: rot30_scale_translate(1.0, 10.0, 4.0),
paths: vec![vec![
Cmd::MoveTo(6.5, 3.25),
Cmd::LineTo(7.125, 24.5),
Cmd::LineTo(8.25, 3.25),
Cmd::Close,
]],
composite: None,
buffer: None,
},
Case {
name: "shallow_wedge",
mask_w: 48,
mask_h: 20,
fill_rule: FillRule::NonZero,
clip: None,
xform: identity(),
paths: vec![vec![
Cmd::MoveTo(3.5, 5.0),
Cmd::LineTo(44.5, 12.25),
Cmd::LineTo(44.5, 10.75),
Cmd::Close,
]],
composite: None,
buffer: None,
},
Case {
name: "two_overlap_nonzero",
mask_w: 40,
mask_h: 30,
fill_rule: FillRule::NonZero,
clip: None,
xform: identity(),
paths: vec![
rect(4.5, 4.25, 24.75, 18.5),
vec![
Cmd::MoveTo(14.25, 9.75),
Cmd::LineTo(34.5, 6.5),
Cmd::LineTo(30.75, 25.25),
Cmd::LineTo(12.5, 20.5),
Cmd::Close,
],
],
composite: None,
buffer: None,
},
Case {
name: "star_nonzero",
mask_w: 40,
mask_h: 40,
fill_rule: FillRule::NonZero,
clip: None,
xform: identity(),
paths: vec![star()],
composite: Some(composite_light_on_dark),
buffer: Some(buffer_over_translucent_slate),
},
Case {
name: "star_evenodd",
mask_w: 40,
mask_h: 40,
fill_rule: FillRule::EvenOdd,
clip: None,
xform: identity(),
paths: vec![star()],
composite: None,
buffer: None,
},
Case {
name: "clip_frac",
mask_w: 48,
mask_h: 30,
fill_rule: FillRule::NonZero,
clip: Some((7.25, 5.5, 15.5, 9.25)),
xform: identity(),
paths: vec![rect(5.25, 4.75, 40.5, 24.25)],
composite: None,
buffer: None,
},
Case {
name: "circle_cubic",
mask_w: 40,
mask_h: 40,
fill_rule: FillRule::NonZero,
clip: None,
xform: identity(),
paths: vec![circle(20.0, 20.0, 15.5)],
composite: None,
buffer: None,
},
Case {
name: "circle_cubic_rot30_scale",
mask_w: 56,
mask_h: 56,
fill_rule: FillRule::NonZero,
clip: None,
xform: rot30_scale_translate(1.75, 28.0, 26.0),
paths: vec![circle(0.0, 0.0, 12.25)],
composite: None,
buffer: None,
},
]
}
fn to_path_storage(cmds: &[Cmd]) -> PathStorage {
let mut path = PathStorage::new();
for cmd in cmds {
match *cmd {
Cmd::MoveTo(x, y) => path.move_to(x, y),
Cmd::LineTo(x, y) => path.line_to(x, y),
Cmd::Curve4(x1, y1, x2, y2, x3, y3) => path.curve4(x1, y1, x2, y2, x3, y3),
Cmd::Close => path.close_polygon(PATH_FLAGS_NONE),
}
}
path
}
fn build_mask(case: &Case, gray: bool) -> LcdMask {
let mut builder = LcdMaskBuilder::new(case.mask_w, case.mask_h)
.with_clip(case.clip)
.with_fill_rule(case.fill_rule);
let mut paths: Vec<PathStorage> = case.paths.iter().map(|p| to_path_storage(p)).collect();
builder.with_paths(&case.xform, |add| {
for path in paths.iter_mut() {
add(path);
}
});
if gray {
builder.finalize_gray()
} else {
builder.finalize()
}
}
fn make_dst(spec: &CompositeCase) -> Vec<u8> {
let mut dst = vec![0u8; (spec.dst_w as usize) * (spec.dst_h as usize) * 4];
for y in 0..spec.dst_h as i32 {
for x in 0..spec.dst_w as i32 {
let rgba = match spec.fill {
DstFill::Solid(c) => c,
DstFill::Halves(split, a, b) => {
if x < split {
a
} else {
b
}
}
};
let i = ((y as usize) * (spec.dst_w as usize) + (x as usize)) * 4;
dst[i..i + 4].copy_from_slice(&rgba);
}
}
dst
}
fn byte_color(rgba: [u8; 4]) -> Color {
Color::rgba(
rgba[0] as f32 / 255.0,
rgba[1] as f32 / 255.0,
rgba[2] as f32 / 255.0,
rgba[3] as f32 / 255.0,
)
}
fn num(v: f64) -> String {
format!("{}#{:016x}", v, v.to_bits())
}
fn fill_rule_name(rule: FillRule) -> &'static str {
match rule {
FillRule::NonZero => "nonzero",
FillRule::EvenOdd => "evenodd",
}
}
fn path_line(cmds: &[Cmd]) -> String {
let mut out = String::from("path");
for cmd in cmds {
match *cmd {
Cmd::MoveTo(x, y) => {
out.push_str(&format!(" m {} {}", num(x), num(y)));
}
Cmd::LineTo(x, y) => {
out.push_str(&format!(" l {} {}", num(x), num(y)));
}
Cmd::Curve4(x1, y1, x2, y2, x3, y3) => {
out.push_str(&format!(
" c {} {} {} {} {} {}",
num(x1),
num(y1),
num(x2),
num(y2),
num(x3),
num(y3)
));
}
Cmd::Close => out.push_str(" z"),
}
}
out
}
fn agg_rust_version() -> &'static str {
let mut lines = CARGO_LOCK.lines();
while let Some(line) = lines.next() {
if line.trim() != "name = \"agg-rust\"" {
continue;
}
for following in lines.by_ref() {
let following = following.trim();
if following.is_empty() {
return "unknown";
}
if let Some(rest) = following.strip_prefix("version = \"") {
return rest.trim_end_matches('"');
}
}
}
"unknown"
}
fn git(args: &[&str]) -> Option<String> {
let output = Command::new("git")
.arg("-C")
.arg(env!("CARGO_MANIFEST_DIR"))
.args(args)
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|text| text.trim().to_string())
}
fn agg_gui_commit(explicit: Option<String>) -> String {
if let Some(sha) = explicit {
return sha;
}
match git(&["rev-parse", "HEAD"]) {
Some(sha) if !sha.is_empty() => {
let dirty = git(&["status", "--porcelain"]).is_some_and(|s| !s.is_empty());
if dirty {
format!("{sha}-dirty")
} else {
sha
}
}
_ => "unknown".to_string(),
}
}
fn write_blob(dir: &Path, name: &str, bytes: &[u8]) {
let path: PathBuf = dir.join(name);
fs::write(&path, bytes).unwrap_or_else(|e| panic!("writing {}: {e}", path.display()));
}
fn main() {
let mut args = std::env::args().skip(1);
let out_dir = PathBuf::from(
args.next()
.unwrap_or_else(|| "target/lcd_reference_fixture".to_string()),
);
let commit = agg_gui_commit(args.next());
fs::create_dir_all(&out_dir).expect("creating output directory");
let primary = agg_gui::font_settings::current_primary_weight();
let gamma = agg_gui::font_settings::current_gamma();
assert!(
(primary - 1.0 / 3.0).abs() < 1e-12 && (gamma - 1.0).abs() < 1e-12,
"fixture requires the default filter parameters (primary 1/3, gamma 1), \
got primary={primary} gamma={gamma}"
);
let mut manifest = String::new();
manifest.push_str("# agg-gui LCD coverage reference fixture\n");
manifest.push_str("# Generated by agg-gui/examples/lcd_reference_fixture.rs\n");
manifest.push_str("# Numbers are <decimal>#<raw f64 bits, 16 hex digits>; parse the bits.\n");
manifest.push_str("# Clip is left/bottom/right/top in mask pixels (right = x + w).\n");
manifest.push_str("# Blob layouts: .lcd/.gray = width*height*3 packed R,G,B coverage, rows\n");
manifest.push_str("# Y-up (row 0 = bottom). .dst = dst_w*dst_h*4 straight RGBA, also Y-up.\n");
manifest.push_str("# .bufcolor/.bufalpha = LcdBuffer planes, w*h*3 packed R,G,B, Y-up:\n");
manifest.push_str("# premultiplied per-channel colour and per-channel alpha.\n");
manifest.push_str(&format!("# Reference: agg-gui commit {commit}\n"));
manifest.push_str(&format!(
"# Reference: agg-rust {} (workspace Cargo.lock)\n",
agg_rust_version()
));
manifest.push_str("version 1\n");
manifest.push_str(&format!(
"filter primary {} gamma {}\n",
num(primary),
num(gamma)
));
let all = cases();
for case in &all {
let lcd = build_mask(case, false);
let gray = build_mask(case, true);
assert_eq!(lcd.width, case.mask_w);
assert_eq!(lcd.height, case.mask_h);
let lcd_name = format!("{}.lcd", case.name);
let gray_name = format!("{}.gray", case.name);
write_blob(&out_dir, &lcd_name, &lcd.data);
write_blob(&out_dir, &gray_name, &gray.data);
manifest.push_str(&format!("case {}\n", case.name));
manifest.push_str(&format!("size {} {}\n", case.mask_w, case.mask_h));
manifest.push_str(&format!("fill {}\n", fill_rule_name(case.fill_rule)));
match case.clip {
None => manifest.push_str("clip none\n"),
Some((x, y, w, h)) => manifest.push_str(&format!(
"clip {} {} {} {}\n",
num(x),
num(y),
num(x + w),
num(y + h)
)),
}
manifest.push_str(&format!(
"xform {} {} {} {} {} {}\n",
num(case.xform.sx),
num(case.xform.shy),
num(case.xform.shx),
num(case.xform.sy),
num(case.xform.tx),
num(case.xform.ty)
));
for path in &case.paths {
manifest.push_str(&path_line(path));
manifest.push('\n');
}
manifest.push_str(&format!("lcd {} {}\n", lcd_name, lcd.data.len()));
manifest.push_str(&format!("gray {} {}\n", gray_name, gray.data.len()));
if let Some(spec) = case.composite {
let mut dst = make_dst(&spec);
composite_lcd_mask(
&mut dst,
spec.dst_w,
spec.dst_h,
&lcd,
byte_color(spec.src),
spec.origin.0,
spec.origin.1,
);
let dst_name = format!("{}.dst", case.name);
write_blob(&out_dir, &dst_name, &dst);
let fill = match spec.fill {
DstFill::Solid(c) => {
format!("solid {} {} {} {}", c[0], c[1], c[2], c[3])
}
DstFill::Halves(split, a, b) => format!(
"halves {} {} {} {} {} {} {} {} {}",
split, a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3]
),
};
manifest.push_str(&format!(
"composite {} {} {} {} {} src {} {} {} {} dstfill {}\n",
dst_name,
spec.dst_w,
spec.dst_h,
spec.origin.0,
spec.origin.1,
spec.src[0],
spec.src[1],
spec.src[2],
spec.src[3],
fill
));
}
if let Some(spec) = case.buffer {
let mut buffer = LcdBuffer::new(spec.buf_w, spec.buf_h);
buffer.clear(byte_color(spec.clear));
let mut paths: Vec<PathStorage> =
case.paths.iter().map(|p| to_path_storage(p)).collect();
for path in paths.iter_mut() {
buffer.fill_path(
path,
byte_color(spec.src),
&case.xform,
case.clip,
case.fill_rule,
);
}
let color_name = format!("{}.bufcolor", case.name);
let alpha_name = format!("{}.bufalpha", case.name);
write_blob(&out_dir, &color_name, buffer.color_plane());
write_blob(&out_dir, &alpha_name, buffer.alpha_plane());
manifest.push_str(&format!(
"buffer {} {} {} {} clear {} {} {} {} src {} {} {} {}\n",
color_name,
alpha_name,
spec.buf_w,
spec.buf_h,
spec.clear[0],
spec.clear[1],
spec.clear[2],
spec.clear[3],
spec.src[0],
spec.src[1],
spec.src[2],
spec.src[3],
));
}
manifest.push_str("end\n");
}
let manifest_path = out_dir.join("manifest.txt");
fs::write(&manifest_path, manifest).expect("writing manifest");
println!(
"wrote {} cases to {}",
all.len(),
out_dir.canonicalize().unwrap_or(out_dir.clone()).display()
);
}