mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-31 05:56:55 -06:00
Merge pull request #1591
Preserve gradient flip mode in PPTX # Conflicts: # crates/pptx/src/read/dml.rs # crates/pptx/src/write/dml.rs # crates/pptx/tests/it/roundtrip.rs
This commit is contained in:
@@ -108,7 +108,13 @@ pub(crate) fn gradient_param(v: &Value) -> Option<Gradient> {
|
||||
scaled: false,
|
||||
},
|
||||
};
|
||||
Some(Gradient { tile: [0.0; 4], stops, shape, rotate_with_shape: v.get("rotateWithShape").and_then(Value::as_bool).unwrap_or(true) })
|
||||
Some(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops,
|
||||
shape,
|
||||
rotate_with_shape: v.get("rotateWithShape").and_then(Value::as_bool).unwrap_or(true),
|
||||
flip: v.get("flip").and_then(Value::as_str).and_then(deckcraft_model::style::GradientFlip::from_xml),
|
||||
})
|
||||
}
|
||||
|
||||
/// Fill and outline apply to a group's visible members as well as its container. Other edits
|
||||
@@ -555,3 +561,14 @@ pub(super) fn quick_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gradient_parameters_keep_flip_mode() {
|
||||
let gradient = gradient_param(&json!({"stops": [[0, "#ff0000"], [1, "#0000ff"]], "flip": "xy"})).expect("gradient");
|
||||
assert_eq!(gradient.flip, Some(deckcraft_model::style::GradientFlip::Xy));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ pub(super) fn paint(d: &Diagram, sh: &mut Shape, i: usize, light: bool) {
|
||||
shape: GradientShape::Linear { angle: 90.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
tile: [0.0; 4],
|
||||
flip: None,
|
||||
}));
|
||||
sh.line = Some(Line::solid(ColorRef::scheme(SchemeSlot::Lt1), 1.0));
|
||||
sh.effects = Some(Effects { outer_shadow: Some(shadow(8.0, 5.0)), ..Default::default() });
|
||||
|
||||
@@ -72,6 +72,36 @@ pub struct GradientStop {
|
||||
pub color: ColorRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum GradientFlip {
|
||||
None,
|
||||
X,
|
||||
Y,
|
||||
Xy,
|
||||
}
|
||||
|
||||
impl GradientFlip {
|
||||
pub fn xml(self) -> &'static str {
|
||||
match self {
|
||||
GradientFlip::None => "none",
|
||||
GradientFlip::X => "x",
|
||||
GradientFlip::Y => "y",
|
||||
GradientFlip::Xy => "xy",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_xml(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"none" => Some(GradientFlip::None),
|
||||
"x" => Some(GradientFlip::X),
|
||||
"y" => Some(GradientFlip::Y),
|
||||
"xy" => Some(GradientFlip::Xy),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
pub enum GradientShape {
|
||||
@@ -90,6 +120,8 @@ pub enum GradientShape {
|
||||
pub struct Gradient {
|
||||
pub stops: Vec<GradientStop>,
|
||||
pub shape: GradientShape,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub flip: Option<GradientFlip>,
|
||||
#[serde(default = "crate::yes")]
|
||||
pub rotate_with_shape: bool,
|
||||
/// `a:tileRect`: the box the gradient is laid out in, as insets from the shape's box
|
||||
|
||||
@@ -73,8 +73,13 @@ fn a_gradient_without_stops_takes_them_from_its_style() {
|
||||
let p = Presentation::default();
|
||||
let s = &p.slides[0];
|
||||
let ctx = Ctx::for_slide(&p, s).unwrap();
|
||||
let own =
|
||||
style::Gradient { tile: [0.0; 4], stops: vec![], shape: style::GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true };
|
||||
let own = style::Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops: vec![],
|
||||
shape: style::GradientShape::Linear { angle: 45.0, scaled: true },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
};
|
||||
let mut st = ShapeStyle::accent(SchemeSlot::Accent2);
|
||||
st.fill_ref.0 = 3;
|
||||
let sh = Shape { style: Some(st.clone()), fill: Some(Fill::Gradient(own.clone())), ..Default::default() };
|
||||
|
||||
@@ -115,6 +115,7 @@ pub fn default_format() -> FormatScheme {
|
||||
],
|
||||
shape: GradientShape::Linear { angle: 90.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
})
|
||||
};
|
||||
FormatScheme {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
|
||||
use deckcraft_geom::{Xfrm, emu_to_pt};
|
||||
use deckcraft_model::style::{
|
||||
ColorBase, ColorRef, Compound, Dash, Effects, Fill, Glow, Gradient, GradientShape, GradientStop, Line, LineAlign, LineCap, LineEnd, LineJoin,
|
||||
PatternFill, PictureFill, PictureMode, Reflection, Shadow,
|
||||
ColorBase, ColorRef, Compound, Dash, Effects, Fill, Glow, Gradient, GradientFlip, GradientShape, GradientStop, Line, LineAlign, LineCap, LineEnd,
|
||||
LineJoin, PatternFill, PictureFill, PictureMode, Reflection, Shadow,
|
||||
};
|
||||
use deckcraft_model::text::{
|
||||
Action, Align, Anchor, AutoFit, BodyProps, Bullet, Caps, Hyperlink, LevelStyle, ListStyle, ParaProps, Paragraph, Run, RunKind, RunProps, Spacing,
|
||||
@@ -122,7 +122,13 @@ pub fn gradient(f: &El) -> Gradient {
|
||||
GradientShape::Linear { angle: 90.0, scaled: false }
|
||||
};
|
||||
let tile = f.child("tileRect").map(|r| [pct(r, "l"), pct(r, "t"), pct(r, "r"), pct(r, "b")].map(|v| v.unwrap_or(0.0).clamp(-10.0, 10.0)));
|
||||
Gradient { tile: tile.unwrap_or_default(), stops, shape, rotate_with_shape: f.bool("rotWithShape").unwrap_or(true) }
|
||||
Gradient {
|
||||
tile: tile.unwrap_or_default(),
|
||||
stops,
|
||||
shape,
|
||||
flip: f.attr("flip").and_then(GradientFlip::from_xml),
|
||||
rotate_with_shape: f.bool("rotWithShape").unwrap_or(true),
|
||||
}
|
||||
}
|
||||
|
||||
/// `blipFill` (either `a:` or `p:`) → picture fill, registering the image as media.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use deckcraft_geom::{Xfrm, pt_to_emu};
|
||||
use deckcraft_model::style::{
|
||||
ColorBase, ColorRef, Compound, Dash, Effects, Fill, GradientShape, Line, LineAlign, LineCap, LineJoin, PictureFill, PictureMode,
|
||||
ColorBase, ColorRef, Compound, Dash, Effects, Fill, GradientFlip, GradientShape, Line, LineAlign, LineCap, LineJoin, PictureFill, PictureMode,
|
||||
};
|
||||
use deckcraft_model::text::{
|
||||
Action, AutoFit, BodyProps, Bullet, Caps, Hyperlink, ListStyle, ParaProps, Paragraph, RunKind, RunProps, Spacing, Strike, TextBody,
|
||||
@@ -285,7 +285,7 @@ fn shadow(w: &mut W, tag: &str, s: &deckcraft_model::style::Shadow) {
|
||||
}
|
||||
|
||||
fn gradient(w: &mut W, g: &deckcraft_model::style::Gradient) {
|
||||
w.open("a:gradFill", A::new().a("rotWithShape", if g.rotate_with_shape { "1" } else { "0" }));
|
||||
w.open("a:gradFill", A::new().o("flip", g.flip.map(GradientFlip::xml)).a("rotWithShape", if g.rotate_with_shape { "1" } else { "0" }));
|
||||
// No stops: no `a:gsLst`, so the gradient keeps taking them from its style's fill.
|
||||
let mut stops = g.stops.clone();
|
||||
if stops.len() == 1 {
|
||||
|
||||
@@ -46,6 +46,7 @@ fn gradient(angle: f64) -> Fill {
|
||||
shape: GradientShape::Linear { angle, scaled: true },
|
||||
rotate_with_shape: true,
|
||||
tile: [0.0; 4],
|
||||
flip: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ fn style_deck() -> Presentation {
|
||||
shape: GradientShape::Path { path: "circle".into(), focus: [0.2, 0.2, 0.2, 0.2] },
|
||||
rotate_with_shape: false,
|
||||
tile: [0.0; 4],
|
||||
flip: None,
|
||||
}));
|
||||
|
||||
let mut patt = shape(&mut p, "Pattern", 200.0, 260.0, 120.0, 80.0);
|
||||
|
||||
@@ -88,7 +88,8 @@ fn a_gradient_without_stops_is_saved_without_stops() {
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let id = ShapeId(p.alloc_id().expect("ids"));
|
||||
let first = p.slides[0].id;
|
||||
let own = Gradient { tile: [0.0; 4], stops: vec![], shape: GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true };
|
||||
let own =
|
||||
Gradient { tile: [0.0; 4], stops: vec![], shape: GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true, flip: None };
|
||||
p.slide_mut(first).expect("slide").shapes.push(Shape {
|
||||
id,
|
||||
name: "Styled".into(),
|
||||
@@ -119,6 +120,7 @@ fn a_fill_overlay_round_trips() {
|
||||
],
|
||||
shape: GradientShape::Linear { angle: 4.5, scaled: true },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
}),
|
||||
};
|
||||
p.slide_mut(first).expect("slide").shapes.push(Shape {
|
||||
@@ -148,6 +150,7 @@ fn a_gradient_tile_rect_is_read_and_round_trips() {
|
||||
shape: GradientShape::Path { path: "circle".into(), focus: [0.0, 0.0, 1.0, 1.0] },
|
||||
rotate_with_shape: true,
|
||||
tile: [-1.0, -1.0, 0.0, 0.0],
|
||||
flip: None,
|
||||
};
|
||||
let mut p = defaults::new_presentation(None);
|
||||
let id = ShapeId(p.alloc_id().expect("ids"));
|
||||
|
||||
@@ -7,8 +7,8 @@ use deckcraft_geom::Xfrm;
|
||||
use deckcraft_model::anim::{AnimClass, AnimStart, Animation, TextBuild, Transition};
|
||||
use deckcraft_model::chart::{Chart, ChartType};
|
||||
use deckcraft_model::style::{
|
||||
ColorRef, Dash, Effects, Fill, Glow, Gradient, GradientShape, GradientStop, Line, LineEnd, PatternFill, PictureAdjust, PictureFill, PictureMode,
|
||||
Shadow,
|
||||
ColorRef, Dash, Effects, Fill, Glow, Gradient, GradientFlip, GradientShape, GradientStop, Line, LineEnd, PatternFill, PictureAdjust, PictureFill,
|
||||
PictureMode, Shadow,
|
||||
};
|
||||
use deckcraft_model::table::Table;
|
||||
use deckcraft_model::text::{
|
||||
@@ -115,6 +115,8 @@ pub fn rich_deck() -> Presentation {
|
||||
],
|
||||
shape: GradientShape::Linear { angle: 45.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
|
||||
flip: Some(GradientFlip::Xy),
|
||||
}));
|
||||
a.line = Some(Line { width: Some(3.0), dash: Some(Dash::Preset { name: "dash".into() }), ..Line::solid(ColorRef::rgb(Rgba::rgb(0, 0, 0)), 3.0) });
|
||||
a.effects = Some(Effects {
|
||||
@@ -575,6 +577,15 @@ fn line_alignment_survives_two_pptx_round_trips() {
|
||||
assert_eq!(deckcraft_model::style::LineAlign::from_xml("out"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn gradient_flip_mode_survives_two_pptx_round_trips() {
|
||||
let p = rich_deck();
|
||||
let q = round(&round(&p));
|
||||
let gradient = by_name(&q.slides[1], "Gradient Star").fill.as_ref();
|
||||
assert!(matches!(gradient, Some(Fill::Gradient(g)) if g.flip == Some(GradientFlip::Xy)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pictures_media_and_ink_round_trip() {
|
||||
let p = rich_deck();
|
||||
|
||||
@@ -978,6 +978,7 @@ fn a_linear_gradient_ramps_like_powerpoint() {
|
||||
stops: vec![stop(0.0, deckcraft_color::Rgba::rgb(0, 0, 0)), stop(1.0, deckcraft_color::Rgba::rgb(255, 255, 255))],
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
})),
|
||||
line: Some(deckcraft_model::style::Line { fill: Some(Fill::None), ..Default::default() }),
|
||||
..Default::default()
|
||||
@@ -1003,6 +1004,7 @@ fn a_gradient_with_a_stop_off_the_end_stays_linear() {
|
||||
stops: vec![stop(0.2, deckcraft_color::Rgba::rgb(0, 0, 0)), stop(1.0, deckcraft_color::Rgba::rgb(255, 255, 255))],
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
})),
|
||||
line: Some(deckcraft_model::style::Line { fill: Some(Fill::None), ..Default::default() }),
|
||||
..Default::default()
|
||||
@@ -1044,6 +1046,7 @@ fn a_shape_filled_with_the_background_shows_the_slide_background() {
|
||||
stops: vec![stop(0.0, 0), stop(0.5, 0), stop(1.0, 200)],
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
}),
|
||||
});
|
||||
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
@@ -1145,6 +1148,7 @@ fn a_text_gradient_starts_over_on_every_line() {
|
||||
stops: vec![stop(0.0, 0), stop(1.0, 255)],
|
||||
shape: GradientShape::Linear { angle: 90.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
}));
|
||||
}
|
||||
sh.text = Some(t);
|
||||
@@ -1306,6 +1310,7 @@ fn rect_path_gradients_draw_x_shaped_contours() {
|
||||
stops: vec![stop(0.0, 0), stop(0.5, 128), stop(1.0, 255)],
|
||||
shape: GradientShape::Path { path: "rect".into(), focus: [0.5, 0.5, 0.5, 0.5] },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
})),
|
||||
line: Some(deckcraft_model::style::Line { fill: Some(Fill::None), ..Default::default() }),
|
||||
..Default::default()
|
||||
|
||||
@@ -186,6 +186,7 @@ fn radial(focus: [f64; 4]) -> Shape {
|
||||
stops,
|
||||
shape: GradientShape::Path { path: "circle".into(), focus },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
})),
|
||||
line: no_line(),
|
||||
..Default::default()
|
||||
@@ -310,8 +311,13 @@ fn a_pictures_border_lies_outside_the_picture() {
|
||||
fn children_filled_with_the_group_fill_show_one_fill_across_the_group() {
|
||||
use deckcraft_model::style::{Gradient, GradientShape, GradientStop};
|
||||
let stops = vec![GradientStop { pos: 0.0, color: ColorRef::rgb(RED) }, GradientStop { pos: 1.0, color: ColorRef::rgb(Rgba::rgb(0, 0, 255)) }];
|
||||
let grad =
|
||||
Fill::Gradient(Gradient { tile: [0.0; 4], stops, shape: GradientShape::Linear { angle: 0.0, scaled: false }, rotate_with_shape: true });
|
||||
let grad = Fill::Gradient(Gradient {
|
||||
tile: [0.0; 4],
|
||||
stops,
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
});
|
||||
let child = |id: u32, x: f64| Shape {
|
||||
id: ShapeId(id),
|
||||
xfrm: Some(Xfrm::new(x, 0.0, 90.0, 100.0)),
|
||||
@@ -372,7 +378,13 @@ fn a_linear_gradient_spans_its_tile_rect() {
|
||||
use deckcraft_model::style::{Gradient, GradientShape, GradientStop};
|
||||
// Tile rect the left half of the box: the ramp ends half way and stays black after.
|
||||
let stops = vec![GradientStop { pos: 0.0, color: ColorRef::rgb(WHITE) }, GradientStop { pos: 1.0, color: ColorRef::rgb(Rgba::BLACK) }];
|
||||
let g = Gradient { stops, shape: GradientShape::Linear { angle: 0.0, scaled: false }, rotate_with_shape: true, tile: [0.0, 0.0, 0.5, 0.0] };
|
||||
let g = Gradient {
|
||||
stops,
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
tile: [0.0, 0.0, 0.5, 0.0],
|
||||
flip: None,
|
||||
};
|
||||
let sh = Shape {
|
||||
id: ShapeId(20),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 100.0)),
|
||||
@@ -502,6 +514,7 @@ fn gradient_lines_and_their_heads_take_the_gradient() {
|
||||
shape: GradientShape::Linear { angle, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
tile: [0.0; 4],
|
||||
flip: None,
|
||||
})
|
||||
};
|
||||
// Left to right along a 20 pt line: red at its start, blue at its end, and the arrowhead at
|
||||
@@ -656,6 +669,7 @@ fn a_gradient_not_rotating_with_its_shape_keeps_to_the_slide() {
|
||||
shape: GradientShape::Linear { angle: 0.0, scaled: false },
|
||||
rotate_with_shape,
|
||||
tile: [0.0; 4],
|
||||
flip: None,
|
||||
};
|
||||
// 200 × 100 turned a quarter: it covers 150..250 across and 50..250 down.
|
||||
let sh = Shape {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use deckcraft_color::{ColorScheme, ColorTransform, SchemeSlot};
|
||||
use deckcraft_model::ColorRef;
|
||||
use deckcraft_model::style::{Gradient, GradientShape, GradientStop, PatternFill};
|
||||
use deckcraft_model::style::{Gradient, GradientFlip, GradientShape, GradientStop, PatternFill};
|
||||
use egui::{Color32, CornerRadius, Rect, Sense, Stroke, Ui, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -21,21 +21,25 @@ pub fn default_gradient(base: ColorRef, angle: f64) -> Gradient {
|
||||
stops: vec![GradientStop { pos: 0.0, color: base }, GradientStop { pos: 1.0, color: dark }],
|
||||
shape: GradientShape::Linear { angle, scaled: false },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The command parameter for `g` (`shape.fill {gradient}` / `design.background {gradient}`).
|
||||
pub fn gradient_json(g: &Gradient) -> Value {
|
||||
let stops: Vec<Value> = g.stops.iter().map(|s| json!([s.pos, cref_param(&s.color)])).collect();
|
||||
let flip = g.flip.map(GradientFlip::xml);
|
||||
match &g.shape {
|
||||
GradientShape::Linear { angle, .. } => json!({"stops": stops, "kind": "linear", "angle": angle, "rotateWithShape": g.rotate_with_shape}),
|
||||
GradientShape::Linear { angle, .. } => {
|
||||
json!({"stops": stops, "kind": "linear", "angle": angle, "rotateWithShape": g.rotate_with_shape, "flip": flip})
|
||||
}
|
||||
GradientShape::Path { path, focus } => {
|
||||
let kind = match path.as_str() {
|
||||
"rect" => "rectangular",
|
||||
"shape" => "path",
|
||||
_ => "radial",
|
||||
};
|
||||
json!({"stops": stops, "kind": kind, "focus": focus, "rotateWithShape": g.rotate_with_shape})
|
||||
json!({"stops": stops, "kind": kind, "focus": focus, "rotateWithShape": g.rotate_with_shape, "flip": flip})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +146,7 @@ pub fn gradient_editor(ui: &mut Ui, id: &str, g: &Gradient, sc: &ColorScheme) ->
|
||||
stops: vec![GradientStop { pos: 0.0, color: light }, GradientStop { pos: 1.0, color: dark }],
|
||||
shape: GradientShape::Path { path: "circle".into(), focus: [0.5, 0.5, 0.5, 0.5] },
|
||||
rotate_with_shape: true,
|
||||
flip: None,
|
||||
},
|
||||
Gradient {
|
||||
stops: vec![GradientStop { pos: 0.0, color: base.clone() }, GradientStop { pos: 1.0, color: with_alpha(base, 0.0) }],
|
||||
@@ -508,3 +513,20 @@ pub fn pattern_editor(ui: &mut Ui, pt: &PatternFill, sc: &ColorScheme) -> Option
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gradient_json_retains_flip_mode() {
|
||||
let gradient = Gradient {
|
||||
stops: vec![],
|
||||
shape: GradientShape::Linear { angle: 90.0, scaled: false },
|
||||
flip: Some(GradientFlip::Xy),
|
||||
rotate_with_shape: true,
|
||||
tile: [0.0; 4],
|
||||
};
|
||||
assert_eq!(gradient_json(&gradient).get("flip"), Some(&json!("xy")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user