mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 18:13:39 +00:00
Stroke: keep Overprint (overprint)
Photoshop stores Stroke › Overprint as `overprint` in the FrFX descriptor. StrokeFx had no field for it, so an edited stroke was written back without it and a stroke with Overprint on lost it. Add `StrokeFx::overprint` (serde default, so older .pcraft files load), read and write `overprint`, accept it in layer.layerStyle.stroke and show the Overprint checkbox in the Layer Style dialog, as Photoshop does. Rendering is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ fn document_with_strokes(layer_count: usize) -> Result<Document, Box<dyn Error>>
|
||||
size: 6.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(0.8, 0.1, 0.2)),
|
||||
overprint: false,
|
||||
}));
|
||||
layer.advanced.transparency_shapes = false;
|
||||
document.layers.push(layer);
|
||||
|
||||
@@ -289,6 +289,7 @@ fn inside_stroke() -> Effect {
|
||||
size: 2.0,
|
||||
position: StrokePosition::Inside,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 0.0, 0.0)),
|
||||
overprint: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -298,6 +299,7 @@ fn outside_stroke() -> Effect {
|
||||
size: 6.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 0.0, 0.0)),
|
||||
overprint: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -318,6 +320,7 @@ fn shapeless_stroke_tile_skips_match_full_region_across_blending_settings() {
|
||||
size: 6.0,
|
||||
position,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 0.0, 0.0)),
|
||||
overprint: false,
|
||||
})];
|
||||
l.blend = blend;
|
||||
l.fill_opacity = fill;
|
||||
|
||||
@@ -21,7 +21,7 @@ fn px(doc: &Document, x: i32, y: i32) -> [f32; 4] {
|
||||
}
|
||||
|
||||
fn stroke(size: f32, position: StrokePosition, paint: FxPaint) -> Effect {
|
||||
Effect::Stroke(StrokeFx { common: FxCommon::new(BlendMode::Normal, 1.0), size, position, paint })
|
||||
Effect::Stroke(StrokeFx { common: FxCommon::new(BlendMode::Normal, 1.0), size, position, paint, overprint: false })
|
||||
}
|
||||
|
||||
fn blue() -> FxPaint {
|
||||
|
||||
@@ -874,6 +874,7 @@ fn stroke(size: f32, position: StrokePosition) -> Effect {
|
||||
size,
|
||||
position,
|
||||
paint: FxPaint::Color(Color::rgb(0.0, 0.0, 1.0)),
|
||||
overprint: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1587,6 +1588,7 @@ fn upper_stroke_blends_with_the_backdrop_not_the_lower_stroke() {
|
||||
size: 3.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 1.0, 0.0)),
|
||||
overprint: false,
|
||||
});
|
||||
let d = fx_doc(vec![yellow, stroke(8.0, StrokePosition::Outside)]);
|
||||
assert!(close4(px(&d, 8, 20), [1.0, 1.0, 0.0, 1.0]), "{:?}", px(&d, 8, 20));
|
||||
@@ -2517,6 +2519,7 @@ fn nested_parallel_fx_doc() -> Document {
|
||||
size: 3.0,
|
||||
position: StrokePosition::Inside,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 0.0, 0.0)),
|
||||
overprint: false,
|
||||
})];
|
||||
let mut glow = Layer::new("glow", LayerContent::Fill(g(90.0)));
|
||||
glow.opacity = 0.5;
|
||||
|
||||
@@ -166,6 +166,9 @@ pub struct StrokeFx {
|
||||
pub size: f32,
|
||||
pub position: StrokePosition,
|
||||
pub paint: FxPaint,
|
||||
/// Overprint (PSD `overprint`).
|
||||
#[serde(default)]
|
||||
pub overprint: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn effect_defaults(kind: &str) -> Value {
|
||||
"innerGlow" => {
|
||||
json!({"blend": "Screen", "opacity": 75, "color": "#ffffbe", "source": "edge", "choke": 0, "size": 5, "contour": "Linear", "antiAlias": false, "noise": 0})
|
||||
}
|
||||
"stroke" => json!({"size": 3, "position": "outside", "blend": "Normal", "opacity": 100, "color": "#000000"}),
|
||||
"stroke" => json!({"size": 3, "position": "outside", "blend": "Normal", "opacity": 100, "overprint": false, "color": "#000000"}),
|
||||
"colorOverlay" => json!({"blend": "Normal", "color": "#ff0000", "opacity": 100}),
|
||||
"gradientOverlay" => {
|
||||
json!({"blend": "Normal", "opacity": 100, "from": "#000000", "to": "#ffffff", "reverse": false, "style": "linear", "angle": 90, "scale": 100})
|
||||
@@ -510,6 +510,7 @@ fn fresh_effect(kind: &str) -> Option<Effect> {
|
||||
size: 3.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 0.0, 0.0)),
|
||||
overprint: false,
|
||||
}),
|
||||
"colorOverlay" => Effect::ColorOverlay { common: FxCommon::new(BlendMode::Normal, 1.0), color: Color::rgb(1.0, 0.0, 0.0) },
|
||||
"gradientOverlay" => Effect::GradientOverlay { common: FxCommon::new(BlendMode::Normal, 1.0), gradient: Gradient::default(), dither: false },
|
||||
@@ -690,6 +691,7 @@ pub fn overlay_effect(fx: &mut Effect, p: &Value) {
|
||||
_ => StrokePosition::Outside,
|
||||
};
|
||||
}
|
||||
set_bool(p, "overprint", &mut s.overprint);
|
||||
if p.get("from").is_some() {
|
||||
s.paint = FxPaint::Gradient(gradient(p));
|
||||
} else if let Some(c) = color_of(p, "color") {
|
||||
@@ -1005,7 +1007,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
style_cmd!(
|
||||
"stroke",
|
||||
"Stroke…",
|
||||
r##"{"size":px=3,"position":"outside|inside|center","color":"#rrggbb","from":"#rrggbb","to":"#rrggbb","method":"classic|perceptual|linear|smooth"?,"smoothness":0..100=100,"style":str,"angle":deg,"opacity":0..100,"blend":str,"add":bool}"##
|
||||
r##"{"size":px=3,"position":"outside|inside|center","color":"#rrggbb","from":"#rrggbb","to":"#rrggbb","method":"classic|perceptual|linear|smooth"?,"smoothness":0..100=100,"style":str,"angle":deg,"opacity":0..100,"blend":str,"overprint":bool,"add":bool}"##
|
||||
),
|
||||
style_cmd!("colorOverlay", "Color Overlay…", r##"{"color":"#rrggbb","opacity":0..100=100,"blend":str,"add":bool}"##),
|
||||
style_cmd!(
|
||||
@@ -1137,6 +1139,17 @@ mod tests {
|
||||
assert_eq!((t.name.as_str(), t.depth, t.invert), ("Bubbles", -2.0, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stroke_overprint_is_a_param() {
|
||||
let mut s = session();
|
||||
s.execute("layer.layerStyle.stroke", json!({"size": 4, "overprint": true})).unwrap();
|
||||
let Effect::Stroke(st) = &effects(&s)[0] else { panic!() };
|
||||
assert!(st.overprint);
|
||||
s.execute("layer.layerStyle.stroke", json!({"overprint": false})).unwrap();
|
||||
let Effect::Stroke(st) = &effects(&s)[0] else { panic!() };
|
||||
assert!(!st.overprint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_style_command_adds_its_effect() {
|
||||
for kind in ["dropShadow", "innerShadow", "outerGlow", "innerGlow", "stroke", "colorOverlay", "gradientOverlay", "bevelEmboss", "satin"] {
|
||||
@@ -1313,6 +1326,7 @@ mod tests {
|
||||
size: 7.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Gradient(Gradient { stops: vec![(0.0, Color::rgb(0.0, 0.0, 1.0)), (1.0, Color::WHITE)], ..Gradient::default() }),
|
||||
overprint: false,
|
||||
});
|
||||
let r = s
|
||||
.execute(
|
||||
@@ -1349,6 +1363,7 @@ mod tests {
|
||||
size: 3.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::BLACK),
|
||||
overprint: false,
|
||||
});
|
||||
for p in [
|
||||
json!({}),
|
||||
|
||||
@@ -852,8 +852,9 @@ mod tests {
|
||||
use photocraft_color::{BlendMode, Color};
|
||||
use photocraft_doc::{FxCommon, FxPaint, StrokeFx, StrokePosition};
|
||||
let light = GlobalLight::default();
|
||||
let stroke =
|
||||
|size, position| Effect::Stroke(StrokeFx { common: FxCommon::new(BlendMode::Normal, 1.0), size, position, paint: FxPaint::Color(Color::BLACK) });
|
||||
let stroke = |size, position| {
|
||||
Effect::Stroke(StrokeFx { common: FxCommon::new(BlendMode::Normal, 1.0), size, position, paint: FxPaint::Color(Color::BLACK), overprint: false })
|
||||
};
|
||||
// Narrow (< STROKE_CHAMFER_MAX): the chamfer, exactly as before #3646.
|
||||
let narrow = program(&stroke(6.0, StrokePosition::Outside), &light, false);
|
||||
assert!(narrow.fields.iter().any(|(k, _)| *k == FieldKind::StrokeOutside), "a narrow raster outside stroke keeps the chamfer: {:?}", narrow.fields);
|
||||
|
||||
@@ -855,7 +855,13 @@ fn hard_stop_effects_and_gradient_maps_request_cpu() {
|
||||
let paint = FxPaint::Gradient(gradient.clone());
|
||||
let effects = [
|
||||
Effect::GradientOverlay { common: FxCommon::new(BlendMode::Normal, 1.0), gradient, dither: false },
|
||||
Effect::Stroke(StrokeFx { common: FxCommon::new(BlendMode::Normal, 1.0), size: 1.0, position: StrokePosition::Inside, paint: paint.clone() }),
|
||||
Effect::Stroke(StrokeFx {
|
||||
common: FxCommon::new(BlendMode::Normal, 1.0),
|
||||
size: 1.0,
|
||||
position: StrokePosition::Inside,
|
||||
paint: paint.clone(),
|
||||
overprint: false,
|
||||
}),
|
||||
Effect::InnerGlow(glow(paint.clone(), GlowTechnique::Softer, 1.0, 0.0, GlowSource::Edge)),
|
||||
Effect::OuterGlow(glow(paint, GlowTechnique::Softer, 1.0, 0.0, GlowSource::Edge)),
|
||||
];
|
||||
@@ -1340,7 +1346,7 @@ fn bevel(style: BevelStyle, up: bool, size: f32, soften: f32) -> Bevel {
|
||||
}
|
||||
|
||||
fn stroke(size: f32, position: StrokePosition, paint: FxPaint) -> StrokeFx {
|
||||
StrokeFx { common: FxCommon::new(BlendMode::Normal, 0.9), size, position, paint }
|
||||
StrokeFx { common: FxCommon::new(BlendMode::Normal, 0.9), size, position, paint, overprint: false }
|
||||
}
|
||||
|
||||
fn checker_pattern() -> Pattern {
|
||||
|
||||
@@ -213,6 +213,7 @@ fn parse_stroke(d: &Descriptor) -> StrokeFx {
|
||||
_ => StrokePosition::Outside,
|
||||
},
|
||||
paint,
|
||||
overprint: bool_of(d, "overprint"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +405,8 @@ fn write_one(e: &Effect) -> (&'static str, Descriptor) {
|
||||
};
|
||||
let mut d = with_common(Descriptor::new("FrFX"), &s.common)
|
||||
.with("Styl", Value::Enumerated { type_id: Id::new("FStl"), value: Id::new(styl) })
|
||||
.with("Sz ", unit(b"#Pxl", s.size));
|
||||
.with("Sz ", unit(b"#Pxl", s.size))
|
||||
.with("overprint", Value::Boolean(s.overprint));
|
||||
d = match &s.paint {
|
||||
FxPaint::Color(c) => {
|
||||
d.with("PntT", Value::Enumerated { type_id: Id::new("FrFl"), value: Id::new("SClr") }).with("Clr ", Value::Descriptor(color_to_desc(c)))
|
||||
@@ -680,12 +682,14 @@ mod tests {
|
||||
size: 3.0,
|
||||
position: StrokePosition::Inside,
|
||||
paint: FxPaint::Color(Color::rgb(0.0, 1.0, 0.0)),
|
||||
overprint: false,
|
||||
}),
|
||||
Effect::Stroke(StrokeFx {
|
||||
common: FxCommon::new(BlendMode::Multiply, 0.5),
|
||||
size: 6.0,
|
||||
position: StrokePosition::Center,
|
||||
paint: FxPaint::Gradient(g.clone()),
|
||||
overprint: true,
|
||||
}),
|
||||
Effect::ColorOverlay { common: FxCommon::new(BlendMode::Overlay, 0.5), color: Color::rgb(0.0, 0.0, 1.0) },
|
||||
Effect::GradientOverlay { common: FxCommon::new(BlendMode::Normal, 1.0), gradient: g, dither: true },
|
||||
|
||||
@@ -8,7 +8,7 @@ use photocraft_geom::{Rect, Size};
|
||||
use photocraft_io::{ExportOptions, export, import};
|
||||
|
||||
fn stroke(size: f32, position: StrokePosition, paint: FxPaint, opacity: f32) -> Effect {
|
||||
Effect::Stroke(StrokeFx { common: FxCommon::new(BlendMode::Normal, opacity), size, position, paint })
|
||||
Effect::Stroke(StrokeFx { common: FxCommon::new(BlendMode::Normal, opacity), size, position, paint, overprint: false })
|
||||
}
|
||||
|
||||
fn roundtrip(doc: &Document) -> Document {
|
||||
|
||||
@@ -340,12 +340,14 @@ fn effects_roundtrip_typed_and_raw() {
|
||||
size: 3.0,
|
||||
position: StrokePosition::Center,
|
||||
paint: FxPaint::Color(photocraft_color::Color::rgb(0.0, 1.0, 0.0)),
|
||||
overprint: false,
|
||||
}),
|
||||
Effect::Stroke(StrokeFx {
|
||||
common: FxCommon::new(photocraft_color::BlendMode::Normal, 0.5),
|
||||
size: 6.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(photocraft_color::Color::rgb(0.0, 0.0, 1.0)),
|
||||
overprint: false,
|
||||
}),
|
||||
];
|
||||
let back = roundtrip(&d);
|
||||
|
||||
@@ -46,6 +46,7 @@ fn effects() -> Vec<Effect> {
|
||||
size: 6.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(0.95, 0.85, 0.2)),
|
||||
overprint: false,
|
||||
}),
|
||||
Effect::BevelEmboss(Bevel {
|
||||
enabled: true,
|
||||
|
||||
@@ -97,6 +97,7 @@ fn text(name: &str, r: Rect) -> Layer {
|
||||
size: 4.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 1.0, 1.0)),
|
||||
overprint: false,
|
||||
}),
|
||||
];
|
||||
l
|
||||
@@ -144,6 +145,7 @@ fn build(w: u32, h: u32) -> Document {
|
||||
size: 3.0,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(0.1, 0.1, 0.1)),
|
||||
overprint: false,
|
||||
}),
|
||||
];
|
||||
d.layers.push(band);
|
||||
|
||||
@@ -141,6 +141,7 @@ fn spec(kind: &str) -> &'static [(&'static str, &'static str, P)] {
|
||||
("position", "Position", P::Choice(POS)),
|
||||
("blend", "Blend Mode", P::Blend),
|
||||
("opacity", "Opacity", P::Slider(0.0, 100.0, "%")),
|
||||
("overprint", "Overprint", P::Check),
|
||||
("color", "Color", P::Color),
|
||||
],
|
||||
"colorOverlay" => &[("blend", "Blend Mode", P::Blend), ("color", "Color", P::Color), ("opacity", "Opacity", P::Slider(0.0, 100.0, "%"))],
|
||||
@@ -266,6 +267,7 @@ fn values_of(e: &Effect, light: f32) -> Value {
|
||||
_ => "outside",
|
||||
}),
|
||||
);
|
||||
set(&mut v, "overprint", json!(s.overprint));
|
||||
if let FxPaint::Color(c) = &s.paint {
|
||||
set(&mut v, "color", json!(hex(c)));
|
||||
} else {
|
||||
@@ -1634,6 +1636,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stroke_overprint_survives_the_dialog() {
|
||||
let mut s = session();
|
||||
s.execute("layer.layerStyle.stroke", json!({"size": 3, "color": "#000000", "overprint": true})).unwrap();
|
||||
let st = s.active().unwrap();
|
||||
let id = st.active_layer.unwrap();
|
||||
let mut f = initial_fields(st.doc.layer(id).unwrap(), None, st.doc.global_light.angle);
|
||||
assert_eq!(effects_of(&f)[0]["params"]["overprint"], json!(true));
|
||||
// Editing another setting keeps Overprint.
|
||||
set_param(&mut f, "fx1", "size", json!(5));
|
||||
apply(&f, |cmd, p| s.execute(cmd, p).map_err(|e| e.to_string())).unwrap();
|
||||
let strokes = strokes_of(&s);
|
||||
assert_eq!(strokes[0].size, 5.0);
|
||||
assert!(strokes[0].overprint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_applies_the_style_without_touching_the_document() {
|
||||
let s = session();
|
||||
|
||||
@@ -429,6 +429,7 @@ mod tests {
|
||||
size,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(1.0, 0.0, 0.0)),
|
||||
overprint: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ fn stroke_fx(size: f32, c: [f32; 3]) -> Effect {
|
||||
size,
|
||||
position: StrokePosition::Outside,
|
||||
paint: FxPaint::Color(Color::rgb(c[0], c[1], c[2])),
|
||||
overprint: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ remaining gaps in #1606 stay tracked with per-sample corpus ceilings.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | Stroke › Overprint (`overprint`) is read and written, set by the stroke command's `overprint` and shown in the Layer Style dialog; an edited stroke no longer drops it. Rendering ignores it for now |
|
||||
| 2026-10-14 | minor | PSD smart filter masks: Photoshop stores a smart object's filtered pixels without its filter mask and mixes the unfiltered ones (`FEid`) back in; import now mixes them and export writes them that way, so sf-filter-mask-hard, -gradient and sf-stack-blend-and-mask pass; Photoshop-oracle floor 185 → 188 of 258 |
|
||||
| 2026-10-13 | minor | Lossy DNG (compression 34892) decodes: 8-bit baseline JPEG tiles and strips, CFA and LinearRaw, partial edge tiles; the stored values take the normal levels and LinearizationTable (FILE-215-11, lossy DNG part) |
|
||||
| 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 |
|
||||
|
||||
Reference in New Issue
Block a user