Merge pull request #3702 from Taha-Mahmoodi/fix/3233-dimension-cmyk

Fix #3233: in a CMYK document dimension lines and labels are CMYK, black on the K plate only
This commit is contained in:
BFlat
2026-10-23 03:47:44 +07:00
committed by GitHub
3 changed files with 79 additions and 12 deletions
+23 -11
View File
@@ -4,7 +4,7 @@ use serde_json::{Value, json};
use std::sync::Arc;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::dimension::{ArrowStyle, Dimension, DimensionMode, DimensionOptions, DimensionSource, LineType, TextAlignment, TextPosition};
use vectorcraft_doc::{Appearance, Arrowhead, CharStyle, Dash, Document, Node, NodeId, NodeKind, TextObject};
use vectorcraft_doc::{Appearance, Arrowhead, CharStyle, ColorMode, Dash, Document, Node, NodeId, NodeKind, TextObject};
use vectorcraft_geom::{Affine, BezPath, PathData, Point, Vec2};
pub fn specs() -> Vec<CommandSpec> {
@@ -234,8 +234,19 @@ pub(crate) fn refresh(before: &Document, doc: &mut Document) -> Result<()> {
Ok(())
}
fn paint(c: [u8; 3]) -> Paint {
Paint::solid(Color::rgb8(c[0], c[1], c[2]))
/// Option colour `c` as a paint in the document's colour model: in a CMYK document black is K
/// only (a rich four-ink black would print a black dimension in every plate) and other colours
/// convert through the active colour settings.
fn paint(c: [u8; 3], mode: ColorMode) -> Paint {
let rgb = Color::rgb8(c[0], c[1], c[2]);
Paint::solid(match mode {
ColorMode::Rgb => rgb,
ColorMode::Cmyk if c == [0, 0, 0] => Color::cmyk(0.0, 0.0, 0.0, 1.0),
ColorMode::Cmyk => {
let cms = vectorcraft_color::cms::active();
cms.convert(&rgb, vectorcraft_color::cms::Model::Cmyk, cms.settings().intent)
}
})
}
fn line(a: Point, b: Point) -> BezPath {
let mut p = BezPath::new();
@@ -244,10 +255,10 @@ fn line(a: Point, b: Point) -> BezPath {
p
}
fn appearance(o: &DimensionOptions, ext: bool, arrows: bool) -> Appearance {
fn appearance(o: &DimensionOptions, mode: ColorMode, ext: bool, arrows: bool) -> Appearance {
let (color, width, kind) =
if ext { (o.extension_color, o.extension_weight, o.extension_type) } else { (o.line_color, o.line_weight, o.line_type) };
let mut a = Appearance::basic(Paint::None, paint(color), width);
let mut a = Appearance::basic(Paint::None, paint(color, mode), width);
if let Some(st) = a.stroke_mut() {
st.dash = match kind {
LineType::Dashed => Some(Dash { pattern: vec![width * 5.0, width * 3.0], ..Default::default() }),
@@ -285,6 +296,7 @@ fn along(o: &DimensionOptions) -> f64 {
fn artwork(doc: &mut Document, d: &Dimension, old: &[NodeId]) -> Result<Vec<Arc<Node>>> {
d.validate().map_err(error)?;
let o = &d.options;
let mode = doc.color_mode;
let label = d.label().map_err(error)?;
let mut paths: Vec<(BezPath, Appearance)> = Vec::new();
let mut angle = 0.0;
@@ -301,11 +313,11 @@ fn artwork(doc: &mut Document, d: &Dimension, old: &[NodeId]) -> Result<Vec<Arc<
for i in 1..=64 {
arc.line_to(on(i as f64 / 64.0));
}
paths.push((arc, appearance(o, false, true)));
paths.push((arc, appearance(o, mode, false, true)));
if !o.hide_extensions {
for p in [d.a, d.b] {
let dir = (p - d.vertex) / (p - d.vertex).hypot();
paths.push((line(d.vertex + dir * o.extension_offset, d.vertex + dir * (r + o.extension_length)), appearance(o, true, false)));
paths.push((line(d.vertex + dir * o.extension_offset, d.vertex + dir * (r + o.extension_length)), appearance(o, mode, true, false)));
}
}
let dir = Vec2::from_angle(start + sweep * along(o));
@@ -333,7 +345,7 @@ fn artwork(doc: &mut Document, d: &Dimension, old: &[NodeId]) -> Result<Vec<Arc<
};
if d.mode == DimensionMode::Linear && !o.hide_extensions {
for (p, q) in [(d.a, a), (d.b, b)] {
paths.push((line(p + outward * o.extension_offset, q + outward * o.extension_length), appearance(o, true, false)));
paths.push((line(p + outward * o.extension_offset, q + outward * o.extension_length), appearance(o, mode, true, false)));
}
}
angle = u.atan2();
@@ -342,13 +354,13 @@ fn artwork(doc: &mut Document, d: &Dimension, old: &[NodeId]) -> Result<Vec<Arc<
}
let shift = o.text_offset + o.font_size * 0.5;
text_at = a.lerp(b, along(o)) + outward * if away { shift } else { -shift };
paths.push((line(a, b), appearance(o, false, true)));
paths.push((line(a, b), appearance(o, mode, false, true)));
}
let style = CharStyle {
font_family: o.font_family.clone(),
font_style: o.font_style.clone(),
size: o.font_size,
fill: paint(o.text_color),
fill: paint(o.text_color, mode),
..Default::default()
};
let mut text = TextObject::point(Point::ZERO, &label, style);
@@ -366,7 +378,7 @@ fn artwork(doc: &mut Document, d: &Dimension, old: &[NodeId]) -> Result<Vec<Arc<
}
let id = old.get(children.len()).copied().unwrap_or_else(|| doc.alloc_id());
let mut node = Node::new(id, NodeKind::Text(Box::new(text)));
node.appearance = Appearance::basic(paint(o.text_color), Paint::None, 0.0).into();
node.appearance = Appearance::basic(paint(o.text_color, mode), Paint::None, 0.0).into();
children.push(Arc::new(node));
Ok(children)
}
+54
View File
@@ -452,3 +452,57 @@ fn both_points_snap_onto_anchors_with_a_visible_indicator() {
run(&mut s, "object.scale", json!({"sx":200,"sy":100}));
assert_eq!(text(&s, id), "144.00 pt", "linked to both anchors");
}
/// The page content streams of exported PDF `v`, inflated.
fn pdf_content(v: &Value) -> String {
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
let mut out = String::new();
let mut rest = &bytes[..];
while let Some(at) = rest.windows(7).position(|w| w == b"stream\n" || w == b"stream\r") {
let body = &rest[at + 7..];
let body = body.strip_prefix(b"\n").unwrap_or(body);
let end = body.windows(9).position(|w| w == b"endstream").unwrap_or(body.len());
let mut raw = Vec::new();
if std::io::Read::read_to_end(&mut flate2::read::ZlibDecoder::new(&body[..end]), &mut raw).is_ok() {
out.push_str(&String::from_utf8_lossy(&raw));
}
rest = &body[(end + 9).min(body.len())..];
}
out
}
/// #3233: in a CMYK document a dimension's black lines and label print on the black plate only
/// (`0 0 0 1 K`), not as a four-ink rich black; other colours convert to CMYK; RGB documents keep
/// RGB.
#[test]
fn dimension_colours_follow_the_document_colour_model() {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width":600,"height":500,"colorMode":"cmyk"}));
let line = edge(&mut s);
run(&mut s, "object.hide", json!({"ids":[line]}));
let id = dim(&mut s, line);
let colours = |s: &Session, id: u64| {
let mut v = Vec::new();
s.doc().unwrap().doc.node(NodeId(id)).unwrap().walk(&mut |n| {
v.extend([n.appearance.fill_paint(), n.appearance.stroke_paint()].iter().filter_map(|p| p.color()));
});
v
};
let black = colours(&s, id);
assert!(!black.is_empty() && black.iter().all(|c| *c == vectorcraft_color::Color::cmyk(0.0, 0.0, 0.0, 1.0)), "{black:?}");
let pdf = pdf_content(&run(&mut s, "document.exportPdf", json!({"preset":"PDF/X-4:2010"})));
// PDF/X-4 paints in the output intent's CMYK space: `/c0 CS 0 0 0 1 SCN` strokes, `scn` fills.
let operands = |op: &str| pdf.match_indices(op).map(|(i, _)| pdf[i.saturating_sub(7)..i].to_string()).collect::<Vec<_>>();
let (strokes, fills) = (operands(" SCN"), operands(" scn"));
assert!(!strokes.is_empty() && !fills.is_empty(), "{pdf}");
assert!(strokes.iter().chain(&fills).all(|v| v == "0 0 0 1"), "K-only colours: {strokes:?} {fills:?}");
run(&mut s, "dimension.update", json!({"ids":[id],"options":{"lineColor":[255,0,0]}}));
let red = colours(&s, id);
assert!(red.iter().any(|c| matches!(c, vectorcraft_color::Color::Cmyk { m, y, .. } if *m > 0.5 && *y > 0.5)), "{red:?}");
assert!(red.iter().all(|c| matches!(c, vectorcraft_color::Color::Cmyk { .. })), "{red:?}");
// RGB documents are unchanged.
let mut s = session();
let line = edge(&mut s);
let id = dim(&mut s, line);
assert!(colours(&s, id).iter().all(|c| *c == vectorcraft_color::Color::rgb8(0, 0, 0)));
}
File diff suppressed because one or more lines are too long