mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-26 03:27:03 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75f4d80e2b |
@@ -483,6 +483,7 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
background: None,
|
background: None,
|
||||||
show_master_shapes: true,
|
show_master_shapes: true,
|
||||||
preserve: true,
|
preserve: true,
|
||||||
|
color_map: None,
|
||||||
raw_ext: None,
|
raw_ext: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
|||||||
background: None,
|
background: None,
|
||||||
show_master_shapes: true,
|
show_master_shapes: true,
|
||||||
preserve: true,
|
preserve: true,
|
||||||
|
color_map: None,
|
||||||
raw_ext: None,
|
raw_ext: None,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+35
-15
@@ -488,6 +488,10 @@ pub struct Slide {
|
|||||||
/// Speaker notes.
|
/// Speaker notes.
|
||||||
pub notes: TextBody,
|
pub notes: TextBody,
|
||||||
pub comments: Vec<Comment>,
|
pub comments: Vec<Comment>,
|
||||||
|
/// `clrMapOvr`: a colour map replacing the layout's or master's (`bg1`→`dk1`…); None follows
|
||||||
|
/// the layout.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub color_map: Option<Vec<(String, String)>>,
|
||||||
/// Unmodelled XML kept for round-trip (custom data, tags).
|
/// Unmodelled XML kept for round-trip (custom data, tags).
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub raw_ext: Option<String>,
|
pub raw_ext: Option<String>,
|
||||||
@@ -507,6 +511,7 @@ impl Default for Slide {
|
|||||||
animations: vec![],
|
animations: vec![],
|
||||||
notes: TextBody::default(),
|
notes: TextBody::default(),
|
||||||
comments: vec![],
|
comments: vec![],
|
||||||
|
color_map: None,
|
||||||
raw_ext: None,
|
raw_ext: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,6 +601,10 @@ pub struct Layout {
|
|||||||
pub background: Option<Background>,
|
pub background: Option<Background>,
|
||||||
pub show_master_shapes: bool,
|
pub show_master_shapes: bool,
|
||||||
pub preserve: bool,
|
pub preserve: bool,
|
||||||
|
/// `clrMapOvr`: a colour map replacing the master's for this layout and the slides using it
|
||||||
|
/// (dark "section" layouts map `bg1` to `dk1`); None follows the master.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub color_map: Option<Vec<(String, String)>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub raw_ext: Option<String>,
|
pub raw_ext: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -641,24 +650,17 @@ impl Master {
|
|||||||
}
|
}
|
||||||
/// Resolve `bg1`/`tx1`/`bg2`/`tx2` through the colour map.
|
/// Resolve `bg1`/`tx1`/`bg2`/`tx2` through the colour map.
|
||||||
pub fn map_slot(&self, slot: SchemeSlot) -> SchemeSlot {
|
pub fn map_slot(&self, slot: SchemeSlot) -> SchemeSlot {
|
||||||
let key = match slot {
|
map_slot(&self.color_map, slot)
|
||||||
SchemeSlot::Bg1 => "bg1",
|
|
||||||
SchemeSlot::Tx1 => "tx1",
|
|
||||||
SchemeSlot::Bg2 => "bg2",
|
|
||||||
SchemeSlot::Tx2 => "tx2",
|
|
||||||
other => return other,
|
|
||||||
};
|
|
||||||
self.color_map.iter().find(|(k, _)| k == key).and_then(|(_, v)| SchemeSlot::from_xml(v)).unwrap_or(match slot {
|
|
||||||
SchemeSlot::Bg1 => SchemeSlot::Lt1,
|
|
||||||
SchemeSlot::Tx1 => SchemeSlot::Dk1,
|
|
||||||
SchemeSlot::Bg2 => SchemeSlot::Lt2,
|
|
||||||
_ => SchemeSlot::Dk2,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/// The theme's colour scheme with the colour map applied (`bg1`… resolve to mapped colours).
|
/// The theme's colour scheme with the colour map applied (`bg1`… resolve to mapped colours).
|
||||||
pub fn scheme(&self) -> ColorScheme {
|
pub fn scheme(&self) -> ColorScheme {
|
||||||
|
self.scheme_with(&self.color_map)
|
||||||
|
}
|
||||||
|
/// The theme's colour scheme with `map` (a layout's or slide's override, or the master's own)
|
||||||
|
/// applied.
|
||||||
|
pub fn scheme_with(&self, map: &[(String, String)]) -> ColorScheme {
|
||||||
let mut s = self.theme.colors.clone();
|
let mut s = self.theme.colors.clone();
|
||||||
if self.color_map.is_empty() {
|
if map.is_empty() {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
let base = self.theme.colors.clone();
|
let base = self.theme.colors.clone();
|
||||||
@@ -668,12 +670,30 @@ impl Master {
|
|||||||
(SchemeSlot::Bg2, SchemeSlot::Lt2),
|
(SchemeSlot::Bg2, SchemeSlot::Lt2),
|
||||||
(SchemeSlot::Tx2, SchemeSlot::Dk2),
|
(SchemeSlot::Tx2, SchemeSlot::Dk2),
|
||||||
] {
|
] {
|
||||||
s.set(slot, base.get(self.map_slot(alias)));
|
s.set(slot, base.get(map_slot(map, alias)));
|
||||||
}
|
}
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve `bg1`/`tx1`/`bg2`/`tx2` through the colour map `map` (the default mapping when it has
|
||||||
|
/// no entry).
|
||||||
|
pub fn map_slot(map: &[(String, String)], slot: SchemeSlot) -> SchemeSlot {
|
||||||
|
let key = match slot {
|
||||||
|
SchemeSlot::Bg1 => "bg1",
|
||||||
|
SchemeSlot::Tx1 => "tx1",
|
||||||
|
SchemeSlot::Bg2 => "bg2",
|
||||||
|
SchemeSlot::Tx2 => "tx2",
|
||||||
|
other => return other,
|
||||||
|
};
|
||||||
|
map.iter().find(|(k, _)| k == key).and_then(|(_, v)| SchemeSlot::from_xml(v)).unwrap_or(match slot {
|
||||||
|
SchemeSlot::Bg1 => SchemeSlot::Lt1,
|
||||||
|
SchemeSlot::Tx1 => SchemeSlot::Dk1,
|
||||||
|
SchemeSlot::Bg2 => SchemeSlot::Lt2,
|
||||||
|
_ => SchemeSlot::Dk2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct Section {
|
pub struct Section {
|
||||||
|
|||||||
@@ -22,23 +22,27 @@ pub struct Ctx<'a> {
|
|||||||
pub layout: Option<&'a Layout>,
|
pub layout: Option<&'a Layout>,
|
||||||
pub owner: Owner,
|
pub owner: Owner,
|
||||||
pub scheme: ColorScheme,
|
pub scheme: ColorScheme,
|
||||||
|
/// The colour map in force: the slide's override, else its layout's, else the master's.
|
||||||
|
pub color_map: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Ctx<'a> {
|
impl<'a> Ctx<'a> {
|
||||||
pub fn for_slide(pres: &'a Presentation, slide: &Slide) -> Option<Ctx<'a>> {
|
pub fn for_slide(pres: &'a Presentation, slide: &Slide) -> Option<Ctx<'a>> {
|
||||||
let (master, layout) = pres.master_for(slide)?;
|
let (master, layout) = pres.master_for(slide)?;
|
||||||
Some(Ctx { pres, master, layout, owner: Owner::Slide, scheme: master.scheme() })
|
let map = slide.color_map.as_ref().or(layout.and_then(|l| l.color_map.as_ref())).unwrap_or(&master.color_map).clone();
|
||||||
|
Some(Ctx { pres, master, layout, owner: Owner::Slide, scheme: master.scheme_with(&map), color_map: map })
|
||||||
}
|
}
|
||||||
pub fn for_layout(pres: &'a Presentation, master: &'a Master, layout: &'a Layout) -> Ctx<'a> {
|
pub fn for_layout(pres: &'a Presentation, master: &'a Master, layout: &'a Layout) -> Ctx<'a> {
|
||||||
Ctx { pres, master, layout: Some(layout), owner: Owner::Layout, scheme: master.scheme() }
|
let map = layout.color_map.as_ref().unwrap_or(&master.color_map).clone();
|
||||||
|
Ctx { pres, master, layout: Some(layout), owner: Owner::Layout, scheme: master.scheme_with(&map), color_map: map }
|
||||||
}
|
}
|
||||||
pub fn for_master(pres: &'a Presentation, master: &'a Master) -> Ctx<'a> {
|
pub fn for_master(pres: &'a Presentation, master: &'a Master) -> Ctx<'a> {
|
||||||
Ctx { pres, master, layout: None, owner: Owner::Master, scheme: master.scheme() }
|
Ctx { pres, master, layout: None, owner: Owner::Master, scheme: master.scheme(), color_map: master.color_map.clone() }
|
||||||
}
|
}
|
||||||
pub fn color(&self, c: &ColorRef, ph: Option<Rgba>) -> Rgba {
|
pub fn color(&self, c: &ColorRef, ph: Option<Rgba>) -> Rgba {
|
||||||
let c = match &c.base {
|
let c = match &c.base {
|
||||||
crate::ColorBase::Scheme { slot } if matches!(slot, SchemeSlot::Bg1 | SchemeSlot::Tx1 | SchemeSlot::Bg2 | SchemeSlot::Tx2) => {
|
crate::ColorBase::Scheme { slot } if matches!(slot, SchemeSlot::Bg1 | SchemeSlot::Tx1 | SchemeSlot::Bg2 | SchemeSlot::Tx2) => {
|
||||||
ColorRef { base: crate::ColorBase::Scheme { slot: self.master.map_slot(*slot) }, mods: c.mods.clone() }
|
ColorRef { base: crate::ColorBase::Scheme { slot: crate::map_slot(&self.color_map, *slot) }, mods: c.mods.clone() }
|
||||||
}
|
}
|
||||||
_ => c.clone(),
|
_ => c.clone(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,3 +159,31 @@ fn hyperlinked_runs_take_the_theme_hyperlink_look() {
|
|||||||
let dbl = resolve::run(&ctx, title, ¶, &RunProps { link, underline: Some("dbl".into()), ..Default::default() });
|
let dbl = resolve::run(&ctx, title, ¶, &RunProps { link, underline: Some("dbl".into()), ..Default::default() });
|
||||||
assert_eq!((dbl.fill, dbl.underline.as_deref()), (hlink, Some("dbl")));
|
assert_eq!((dbl.fill, dbl.underline.as_deref()), (hlink, Some("dbl")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_layout_colour_map_override_applies_to_its_slides() {
|
||||||
|
// A dark "section" layout maps bg1→dk1 and tx1→lt1; a slide on it that keeps the master
|
||||||
|
// mapping (`masterClrMapping`) follows the layout, as PowerPoint draws it.
|
||||||
|
let mut p = Presentation::default();
|
||||||
|
let dark: Vec<(String, String)> =
|
||||||
|
[("bg1", "dk1"), ("tx1", "lt1"), ("bg2", "dk2"), ("tx2", "lt2")].iter().map(|(a, b)| (a.to_string(), b.to_string())).collect();
|
||||||
|
let layout = p.slides[0].layout;
|
||||||
|
let m = std::sync::Arc::make_mut(&mut p.masters[0]);
|
||||||
|
let colors = m.theme.colors.clone();
|
||||||
|
m.layouts.iter_mut().find(|l| l.id == layout).unwrap().color_map = Some(dark.clone());
|
||||||
|
let s: Slide = (*p.slides[0]).clone();
|
||||||
|
let ctx = Ctx::for_slide(&p, &s).unwrap();
|
||||||
|
assert_eq!(ctx.color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Dk1));
|
||||||
|
assert_eq!(ctx.color(&ColorRef::scheme(SchemeSlot::Tx2), None), colors.get(SchemeSlot::Lt2));
|
||||||
|
assert_eq!(ctx.scheme.get(SchemeSlot::Lt1), colors.get(SchemeSlot::Dk1));
|
||||||
|
// The slide's own override wins over the layout's.
|
||||||
|
let mut own = s.clone();
|
||||||
|
own.color_map = Some([("bg1", "lt1"), ("tx1", "dk1")].iter().map(|(a, b)| (a.to_string(), b.to_string())).collect());
|
||||||
|
let ctx = Ctx::for_slide(&p, &own).unwrap();
|
||||||
|
assert_eq!(ctx.color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Lt1));
|
||||||
|
// The layout itself draws with its override; the master keeps its own map.
|
||||||
|
let m = &p.masters[0];
|
||||||
|
let l = m.layout(layout).unwrap();
|
||||||
|
assert_eq!(Ctx::for_layout(&p, m, l).color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Dk1));
|
||||||
|
assert_eq!(Ctx::for_master(&p, m).color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Lt1));
|
||||||
|
}
|
||||||
|
|||||||
@@ -523,12 +523,18 @@ fn read_layout(imp: &mut Imp, part: &Part, doc: &Doc) -> Layout {
|
|||||||
l.shapes = shapes::sp_tree(imp, part, &mut ctx, tree, 0);
|
l.shapes = shapes::sp_tree(imp, part, &mut ctx, tree, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
l.color_map = color_map_override(root);
|
||||||
if let Some(ext) = root.child("extLst").filter(|e| !has_rel_refs(e)) {
|
if let Some(ext) = root.child("extLst").filter(|e| !has_rel_refs(e)) {
|
||||||
l.raw_ext = Some(ext.elements().map(|e| part.keep(e)).collect());
|
l.raw_ext = Some(ext.elements().map(|e| part.keep(e)).collect());
|
||||||
}
|
}
|
||||||
l
|
l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A layout's or slide's `clrMapOvr/overrideClrMapping` (None for `masterClrMapping` or none).
|
||||||
|
fn color_map_override(root: &El) -> Option<Vec<(String, String)>> {
|
||||||
|
root.child("clrMapOvr")?.child("overrideClrMapping").map(color_map)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_slide(imp: &mut Imp, part: &Part, doc: &Doc, id: SlideId, layout: LayoutId) -> Slide {
|
fn read_slide(imp: &mut Imp, part: &Part, doc: &Doc, id: SlideId, layout: LayoutId) -> Slide {
|
||||||
let root = &doc.root;
|
let root = &doc.root;
|
||||||
let mut s = Slide {
|
let mut s = Slide {
|
||||||
@@ -536,6 +542,7 @@ fn read_slide(imp: &mut Imp, part: &Part, doc: &Doc, id: SlideId, layout: Layout
|
|||||||
layout,
|
layout,
|
||||||
hidden: root.bool("show") == Some(false),
|
hidden: root.bool("show") == Some(false),
|
||||||
show_master_shapes: root.bool("showMasterSp").unwrap_or(true),
|
show_master_shapes: root.bool("showMasterSp").unwrap_or(true),
|
||||||
|
color_map: color_map_override(root),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut ctx = shapes::IdCtx::default();
|
let mut ctx = shapes::IdCtx::default();
|
||||||
|
|||||||
@@ -227,6 +227,21 @@ const CLR_KEYS: [(&str, &str); 12] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
fn clr_map(w: &mut W, map: &[(String, String)]) {
|
fn clr_map(w: &mut W, map: &[(String, String)]) {
|
||||||
|
w.empty("p:clrMap", clr_map_attrs(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `p:clrMapOvr`: the layout's or slide's own colour map, else `a:masterClrMapping`.
|
||||||
|
fn clr_map_ovr(w: &mut W, map: Option<&[(String, String)]>) {
|
||||||
|
w.open0("p:clrMapOvr");
|
||||||
|
match map {
|
||||||
|
Some(m) => w.empty("a:overrideClrMapping", clr_map_attrs(m)),
|
||||||
|
None => w.empty0("a:masterClrMapping"),
|
||||||
|
}
|
||||||
|
w.close("p:clrMapOvr");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The twelve colour map attributes, each a theme slot (the default for anything else).
|
||||||
|
fn clr_map_attrs(map: &[(String, String)]) -> A {
|
||||||
let mut a = A::new();
|
let mut a = A::new();
|
||||||
for (k, d) in CLR_KEYS {
|
for (k, d) in CLR_KEYS {
|
||||||
let v = map
|
let v = map
|
||||||
@@ -237,7 +252,7 @@ fn clr_map(w: &mut W, map: &[(String, String)]) {
|
|||||||
.unwrap_or(d);
|
.unwrap_or(d);
|
||||||
a = a.a(k, v);
|
a = a.a(k, v);
|
||||||
}
|
}
|
||||||
w.empty("p:clrMap", a);
|
a
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ext_list(w: &mut W, raw: Option<&str>) {
|
fn ext_list(w: &mut W, raw: Option<&str>) {
|
||||||
@@ -522,9 +537,7 @@ fn layout_xml(x: &mut Exp, o: &mut Out, l: &Layout) -> Vec<u8> {
|
|||||||
A::new().o("showMasterSp", (!l.show_master_shapes).then_some("0")).a("type", l.kind.xml()).t("preserve", l.preserve),
|
A::new().o("showMasterSp", (!l.show_master_shapes).then_some("0")).a("type", l.kind.xml()).t("preserve", l.preserve),
|
||||||
);
|
);
|
||||||
csld(&mut w, x, o, &l.name, l.background.as_ref(), &l.shapes);
|
csld(&mut w, x, o, &l.name, l.background.as_ref(), &l.shapes);
|
||||||
w.open0("p:clrMapOvr");
|
clr_map_ovr(&mut w, l.color_map.as_deref());
|
||||||
w.empty0("a:masterClrMapping");
|
|
||||||
w.close("p:clrMapOvr");
|
|
||||||
ext_list(&mut w, ext_children(l.raw_ext.as_deref()).as_deref());
|
ext_list(&mut w, ext_children(l.raw_ext.as_deref()).as_deref());
|
||||||
w.close("p:sldLayout");
|
w.close("p:sldLayout");
|
||||||
w.finish()
|
w.finish()
|
||||||
@@ -556,9 +569,7 @@ fn slide_xml(x: &mut Exp, o: &mut Out, s: &Slide) -> Vec<u8> {
|
|||||||
let mut w = W::new();
|
let mut w = W::new();
|
||||||
root_open(&mut w, "p:sld", A::new().o("showMasterSp", (!s.show_master_shapes).then_some("0")).o("show", s.hidden.then_some("0")));
|
root_open(&mut w, "p:sld", A::new().o("showMasterSp", (!s.show_master_shapes).then_some("0")).o("show", s.hidden.then_some("0")));
|
||||||
let ids = csld(&mut w, x, o, &s.name, s.background.as_ref(), &s.shapes);
|
let ids = csld(&mut w, x, o, &s.name, s.background.as_ref(), &s.shapes);
|
||||||
w.open0("p:clrMapOvr");
|
clr_map_ovr(&mut w, s.color_map.as_deref());
|
||||||
w.empty0("a:masterClrMapping");
|
|
||||||
w.close("p:clrMapOvr");
|
|
||||||
if let Some(t) = &s.transition {
|
if let Some(t) = &s.transition {
|
||||||
timing::transition(&mut w, x, o, t);
|
timing::transition(&mut w, x, o, t);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,3 +730,39 @@ fn date_fields_keep_their_type_and_saved_text() {
|
|||||||
assert_eq!(by_name(&q.slides[0], name).text, by_name(&p.slides[0], name).text, "{name}");
|
assert_eq!(by_name(&q.slides[0], name).text, by_name(&p.slides[0], name).text, "{name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colour_map_overrides_round_trip() {
|
||||||
|
let mut p = defaults::new_presentation(None);
|
||||||
|
let dark: Vec<(String, String)> =
|
||||||
|
[("bg1", "dk1"), ("tx1", "lt1"), ("bg2", "dk2"), ("tx2", "lt2")].iter().map(|(a, b)| (a.to_string(), b.to_string())).collect();
|
||||||
|
let layout = p.slides[0].layout;
|
||||||
|
std::sync::Arc::make_mut(&mut p.masters[0]).layouts.iter_mut().find(|l| l.id == layout).expect("layout").color_map = Some(dark.clone());
|
||||||
|
let first = p.slides[0].id;
|
||||||
|
p.slide_mut(first).expect("slide").color_map = Some(dark.clone());
|
||||||
|
let q = round(&p);
|
||||||
|
let get = |m: &Option<Vec<(String, String)>>, k: &str| m.as_ref().and_then(|m| m.iter().find(|(a, _)| a == k).map(|(_, v)| v.clone()));
|
||||||
|
let ql = q.masters[0].layouts.iter().find(|l| l.color_map.is_some()).expect("the layout keeps its override");
|
||||||
|
assert_eq!(get(&ql.color_map, "bg1").as_deref(), Some("dk1"));
|
||||||
|
assert_eq!(get(&q.slides[0].color_map, "tx1").as_deref(), Some("lt1"));
|
||||||
|
// Layouts and slides without one keep following the master.
|
||||||
|
assert!(q.masters[0].layouts.iter().filter(|l| l.color_map.is_none()).count() >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_picture_duotone_round_trips() {
|
||||||
|
let mut p = defaults::new_presentation(None);
|
||||||
|
let img = p.add_media("image1.png", "image/png", png(4, 4, [128, 128, 128]));
|
||||||
|
let mut pic = shape(&mut p, "Duotone", 10.0, 10.0, 100.0, 100.0);
|
||||||
|
let mut pf = PictureFill { media: img, ..Default::default() };
|
||||||
|
pf.adjust.duotone = Some((ColorRef::scheme(SchemeSlot::Accent1).with(ColorTransform::Shade(30000)), ColorRef::rgb(Rgba::rgb(250, 240, 200))));
|
||||||
|
pic.kind = deckcraft_model::ShapeKind::Picture { fill: pf };
|
||||||
|
let first = p.slides[0].id;
|
||||||
|
p.slide_mut(first).expect("slide").shapes.push(pic);
|
||||||
|
let q = round(&p);
|
||||||
|
let sh = by_name(&q.slides[0], "Duotone");
|
||||||
|
let deckcraft_model::ShapeKind::Picture { fill } = &sh.kind else { panic!("picture") };
|
||||||
|
let (d, l) = fill.adjust.duotone.as_ref().expect("duotone kept");
|
||||||
|
assert!(matches!(d.base, deckcraft_model::ColorBase::Scheme { slot: SchemeSlot::Accent1 }));
|
||||||
|
assert!(matches!(l.base, deckcraft_model::ColorBase::Rgb { rgb } if rgb == Rgba::rgb(250, 240, 200)));
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,9 +79,26 @@ fn adjust_key(a: &PictureAdjust) -> u64 {
|
|||||||
if let Some(c) = a.clear_color {
|
if let Some(c) = a.clear_color {
|
||||||
mix(((c.r as u64) << 16) | ((c.g as u64) << 8) | c.b as u64 | 1 << 40);
|
mix(((c.r as u64) << 16) | ((c.g as u64) << 8) | c.b as u64 | 1 << 40);
|
||||||
}
|
}
|
||||||
|
if let Some((d, l)) = duotone_rgb(a) {
|
||||||
|
for c in [d, l] {
|
||||||
|
mix(((c.r as u64) << 24) | ((c.g as u64) << 16) | ((c.b as u64) << 8) | c.a as u64 | 1 << 41);
|
||||||
|
}
|
||||||
|
}
|
||||||
h
|
h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The duotone's dark and light colours. The painter resolves them to plain RGB first (see
|
||||||
|
/// `paint::resolved`); a colour that is still a scheme reference falls back to black and white.
|
||||||
|
fn duotone_rgb(a: &PictureAdjust) -> Option<(deckcraft_color::Rgba, deckcraft_color::Rgba)> {
|
||||||
|
use deckcraft_model::style::ColorBase;
|
||||||
|
let (d, l) = a.duotone.as_ref()?;
|
||||||
|
let rgb = |c: &deckcraft_model::style::ColorRef, or: deckcraft_color::Rgba| match &c.base {
|
||||||
|
ColorBase::Rgb { rgb } if c.mods.is_empty() => *rgb,
|
||||||
|
_ => or,
|
||||||
|
};
|
||||||
|
Some((rgb(d, deckcraft_color::Rgba::BLACK), rgb(l, deckcraft_color::Rgba::WHITE)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Decode encoded bytes (PNG, JPEG, GIF, WebP, BMP, TIFF) to straight RGBA.
|
/// Decode encoded bytes (PNG, JPEG, GIF, WebP, BMP, TIFF) to straight RGBA.
|
||||||
pub fn decode(bytes: &[u8]) -> Option<image::RgbaImage> {
|
pub fn decode(bytes: &[u8]) -> Option<image::RgbaImage> {
|
||||||
if bytes.len() > 512 << 20 {
|
if bytes.len() > 512 << 20 {
|
||||||
@@ -233,6 +250,7 @@ fn apply_adjust(img: &mut image::RgbaImage, a: &PictureAdjust) {
|
|||||||
if a.brightness == 0.0 && a.contrast == 0.0 && sat == 1.0 && !a.grayscale && a.clear_color.is_none() && a.duotone.is_none() {
|
if a.brightness == 0.0 && a.contrast == 0.0 && sat == 1.0 && !a.grayscale && a.clear_color.is_none() && a.duotone.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let duo = duotone_rgb(a);
|
||||||
let b = a.brightness.clamp(-1.0, 1.0) * 255.0;
|
let b = a.brightness.clamp(-1.0, 1.0) * 255.0;
|
||||||
let c = a.contrast.clamp(-1.0, 1.0);
|
let c = a.contrast.clamp(-1.0, 1.0);
|
||||||
let cf = if c >= 0.0 { 1.0 / (1.0 - c * 0.99) } else { 1.0 + c };
|
let cf = if c >= 0.0 { 1.0 / (1.0 - c * 0.99) } else { 1.0 + c };
|
||||||
@@ -252,6 +270,12 @@ fn apply_adjust(img: &mut image::RgbaImage, a: &PictureAdjust) {
|
|||||||
*v = y + (*v - y) * s;
|
*v = y + (*v - y) * s;
|
||||||
*v = (*v - 128.0) * cf + 128.0 + b;
|
*v = (*v - 128.0) * cf + 128.0 + b;
|
||||||
}
|
}
|
||||||
|
// Duotone: the pixel's luminance picks a colour between the dark and the light one.
|
||||||
|
if let Some((d, l)) = duo {
|
||||||
|
let t = ((0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255.0).clamp(0.0, 1.0);
|
||||||
|
rgb =
|
||||||
|
[d.r as f64 + (l.r as f64 - d.r as f64) * t, d.g as f64 + (l.g as f64 - d.g as f64) * t, d.b as f64 + (l.b as f64 - d.b as f64) * t];
|
||||||
|
}
|
||||||
for (i, v) in rgb.iter().enumerate() {
|
for (i, v) in rgb.iter().enumerate() {
|
||||||
if let Some(ch) = p.0.get_mut(i) {
|
if let Some(ch) = p.0.get_mut(i) {
|
||||||
*ch = v.round().clamp(0.0, 255.0) as u8;
|
*ch = v.round().clamp(0.0, 255.0) as u8;
|
||||||
@@ -266,6 +290,25 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duotone_maps_luminance_between_its_two_colours() {
|
||||||
|
use deckcraft_color::Rgba;
|
||||||
|
use deckcraft_model::style::ColorRef;
|
||||||
|
let mut img = image::RgbaImage::from_fn(3, 1, |x, _| image::Rgba([[0, 0, 0, 255], [128, 128, 128, 200], [255, 255, 255, 255]][x as usize]));
|
||||||
|
let adj =
|
||||||
|
PictureAdjust { duotone: Some((ColorRef::rgb(Rgba::rgb(100, 40, 0)), ColorRef::rgb(Rgba::rgb(250, 240, 200)))), ..Default::default() };
|
||||||
|
apply_adjust(&mut img, &adj);
|
||||||
|
assert_eq!(img.get_pixel(0, 0).0, [100, 40, 0, 255], "black takes the dark colour");
|
||||||
|
assert_eq!(img.get_pixel(2, 0).0, [250, 240, 200, 255], "white takes the light colour");
|
||||||
|
let mid = img.get_pixel(1, 0).0;
|
||||||
|
assert!((mid[0] as i32 - 175).abs() <= 1 && (mid[1] as i32 - 140).abs() <= 1 && (mid[2] as i32 - 100).abs() <= 1, "{mid:?}");
|
||||||
|
assert_eq!(mid[3], 200, "alpha is kept");
|
||||||
|
// Different duotones are cached separately.
|
||||||
|
let other = PictureAdjust { duotone: Some((ColorRef::rgb(Rgba::rgb(0, 0, 0)), ColorRef::rgb(Rgba::rgb(255, 0, 0)))), ..Default::default() };
|
||||||
|
assert_ne!(adjust_key(&adj), adjust_key(&other));
|
||||||
|
assert_ne!(adjust_key(&adj), adjust_key(&PictureAdjust::default()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_gif_of_many_tiny_frames_keeps_only_the_frame_cap() {
|
fn a_gif_of_many_tiny_frames_keeps_only_the_frame_cap() {
|
||||||
use image::{Delay, Frame, Rgba, RgbaImage};
|
use image::{Delay, Frame, Rgba, RgbaImage};
|
||||||
|
|||||||
@@ -683,7 +683,7 @@ impl Renderer {
|
|||||||
let bounds = Rect::new(0.0, 0.0, w, h);
|
let bounds = Rect::new(0.0, 0.0, w, h);
|
||||||
ctx.push_clip_layer(&outline);
|
ctx.push_clip_layer(&outline);
|
||||||
let t = f.opts.gif_times.iter().find(|g| g.0 == s.id).map(|g| g.1);
|
let t = f.opts.gif_times.iter().find(|g| g.0 == s.id).map(|g| g.1);
|
||||||
paint::picture(ctx, f.pres, pf, bounds, m, t);
|
paint::picture(ctx, f.pres, &paint::resolved(rctx, pf, None), bounds, m, t);
|
||||||
ctx.pop_layer();
|
ctx.pop_layer();
|
||||||
}
|
}
|
||||||
ShapeKind::Media(mc) => {
|
ShapeKind::Media(mc) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use deckcraft_color::Rgba;
|
use deckcraft_color::Rgba;
|
||||||
use deckcraft_geom::preset::FillMode;
|
use deckcraft_geom::preset::FillMode;
|
||||||
use deckcraft_model::resolve::Ctx;
|
use deckcraft_model::resolve::Ctx;
|
||||||
use deckcraft_model::style::{Fill, GradientShape, PictureFill, PictureMode};
|
use deckcraft_model::style::{ColorRef, Fill, GradientShape, PictureFill, PictureMode};
|
||||||
use deckcraft_model::{PhType, Presentation};
|
use deckcraft_model::{PhType, Presentation};
|
||||||
use kurbo::{Affine, BezPath, Point, Rect, Shape as _};
|
use kurbo::{Affine, BezPath, Point, Rect, Shape as _};
|
||||||
use vello_cpu::{RenderContext, peniko};
|
use vello_cpu::{RenderContext, peniko};
|
||||||
@@ -65,7 +65,7 @@ pub fn fill_path(
|
|||||||
}
|
}
|
||||||
Fill::Picture(pf) => {
|
Fill::Picture(pf) => {
|
||||||
ctx.push_clip_layer(path);
|
ctx.push_clip_layer(path);
|
||||||
picture(ctx, pres, pf, bounds, m, None);
|
picture(ctx, pres, &resolved(rctx, pf, ph), bounds, m, None);
|
||||||
ctx.pop_layer();
|
ctx.pop_layer();
|
||||||
}
|
}
|
||||||
Fill::Pattern(p) => {
|
Fill::Pattern(p) => {
|
||||||
@@ -120,6 +120,16 @@ fn p_rect(x: f64, y: f64, w: f64, h: f64) -> BezPath {
|
|||||||
}
|
}
|
||||||
fn p_add(_a: &mut BezPath, _b: &mut BezPath) {}
|
fn p_add(_a: &mut BezPath, _b: &mut BezPath) {}
|
||||||
|
|
||||||
|
/// `pf` with its duotone colours resolved to plain RGB against the slide's colours and the
|
||||||
|
/// placeholder colour `ph` (theme background pictures recolour with `phClr`), so the decoded-image
|
||||||
|
/// cache can apply and key them without a colour scheme.
|
||||||
|
pub fn resolved<'a>(rctx: &Ctx, pf: &'a PictureFill, ph: Option<Rgba>) -> std::borrow::Cow<'a, PictureFill> {
|
||||||
|
let Some((d, l)) = &pf.adjust.duotone else { return std::borrow::Cow::Borrowed(pf) };
|
||||||
|
let mut out = pf.clone();
|
||||||
|
out.adjust.duotone = Some((ColorRef::rgb(rctx.color(d, ph)), ColorRef::rgb(rctx.color(l, ph))));
|
||||||
|
std::borrow::Cow::Owned(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw a picture fill into `bounds` (shape-local) honouring crop and stretch/tile modes; an animated
|
/// Draw a picture fill into `bounds` (shape-local) honouring crop and stretch/tile modes; an animated
|
||||||
/// GIF shows its frame at `gif_time` seconds.
|
/// GIF shows its frame at `gif_time` seconds.
|
||||||
pub fn picture(ctx: &mut RenderContext, pres: &Presentation, pf: &PictureFill, bounds: Rect, m: Affine, gif_time: Option<f64>) {
|
pub fn picture(ctx: &mut RenderContext, pres: &Presentation, pf: &PictureFill, bounds: Rect, m: Affine, gif_time: Option<f64>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user