Pptx/Render: picture fills with rotWithShape=0 stay upright
A shape's picture fill marked not to rotate with the shape turned and flipped with it (rotWithShape was neither kept nor written: always 1). PowerPoint keeps such a picture upright, stretched (or tiled) over the box the rotated shape covers on the slide and clipped to the shape. Now read, written (PictureFill::upright) and drawn so. Wild synthetic set: picture fills on rotated/flipped shapes 12.89 -> 3.87. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ec12412e0
commit
cdaeb2fbde
|
|
@ -139,6 +139,10 @@ pub struct PictureFill {
|
|||
/// 0–1 opacity of the picture.
|
||||
pub alpha: Option<f64>,
|
||||
pub adjust: PictureAdjust,
|
||||
/// `rotWithShape="0"`: the picture keeps to the slide's axes when its shape is rotated or
|
||||
/// flipped.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub upright: bool,
|
||||
}
|
||||
|
||||
/// Picture corrections and colour (Picture Format ▸ Corrections / Color / Transparency).
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ pub fn blip_fill(imp: &mut Imp, part: &Part, bf: &El) -> Option<PictureFill> {
|
|||
let blip = bf.child("blip");
|
||||
let media = blip.and_then(|b| media_ref(imp, part, b));
|
||||
let media = media?;
|
||||
let mut pf = PictureFill { media, ..Default::default() };
|
||||
let mut pf = PictureFill { media, upright: !bf.bool("rotWithShape").unwrap_or(true), ..Default::default() };
|
||||
if let Some(b) = blip {
|
||||
for e in b.elements() {
|
||||
match e.local() {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ pub fn fill(w: &mut W, x: &mut Exp, o: &mut Out, f: &Fill) {
|
|||
/// `a:blipFill` / `p:blipFill`.
|
||||
pub fn blip_fill(w: &mut W, x: &mut Exp, o: &mut Out, tag: &str, pf: &PictureFill) {
|
||||
let rid = x.media_rel(o, pf.media, "image").unwrap_or_else(|| x.placeholder_rel(o));
|
||||
w.open(tag, A::new().a("rotWithShape", "1"));
|
||||
w.open(tag, A::new().a("rotWithShape", if pf.upright { "0" } else { "1" }));
|
||||
let adj = &pf.adjust;
|
||||
let has_kids = pf.alpha.is_some()
|
||||
|| adj.brightness != 0.0
|
||||
|
|
|
|||
|
|
@ -228,3 +228,24 @@ fn black_and_white_and_inverted_alpha_pictures_round_trip() {
|
|||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_upright_picture_fill_round_trips() {
|
||||
let bytes = deck();
|
||||
let edited = edit_slides(&bytes, |x| x.replacen("rotWithShape=\"1\"", "rotWithShape=\"0\"", 1));
|
||||
let q = deckcraft_pptx::import(&edited).expect("import");
|
||||
let s = q.slides[0].shapes.iter().find(|s| s.name == "Textured").expect("shape");
|
||||
let Some(Fill::Picture(pf)) = &s.fill else { panic!("{:?}", s.fill) };
|
||||
assert!(pf.upright);
|
||||
let again = deckcraft_pptx::export(&q).expect("export");
|
||||
let r = deckcraft_pptx::import(&again).expect("import");
|
||||
let s = r.slides[0].shapes.iter().find(|s| s.name == "Textured").expect("shape");
|
||||
assert!(matches!(&s.fill, Some(Fill::Picture(pf)) if pf.upright));
|
||||
// Rotating with the shape is the default.
|
||||
assert!(matches!(textured_fill(&bytes), Some(Fill::Picture(pf)) if !pf.upright));
|
||||
}
|
||||
|
||||
fn textured_fill(bytes: &[u8]) -> Option<Fill> {
|
||||
let q = deckcraft_pptx::import(bytes).expect("import");
|
||||
q.slides[0].shapes.iter().find(|s| s.name == "Textured").and_then(|s| s.fill.clone())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ pub fn rich_deck() -> Presentation {
|
|||
mode: PictureMode::Stretch { fill_rect: [0.0; 4] },
|
||||
alpha: Some(0.5),
|
||||
adjust: PictureAdjust { brightness: 0.2, contrast: -0.1, grayscale: true, ..Default::default() },
|
||||
upright: false,
|
||||
},
|
||||
};
|
||||
let mut tiled = shape(&mut p, "Tiled", 350.0, 120.0, 200.0, 150.0);
|
||||
|
|
|
|||
|
|
@ -82,6 +82,19 @@ pub fn fill_path(
|
|||
ctx.fill_path(path);
|
||||
}
|
||||
Fill::Picture(pf) => {
|
||||
// `rotWithShape="0"`: upright on the slide, stretched over the box the turned shape
|
||||
// covers, clipped to it.
|
||||
let c = m.as_coeffs();
|
||||
let turned = c[1].abs() > 1e-9 || c[2].abs() > 1e-9 || c[0] < 0.0 || c[3] < 0.0;
|
||||
if pf.upright && turned && m.determinant().abs() > 1e-12 {
|
||||
let on_slide = m.transform_rect_bbox(bounds);
|
||||
let on_path = m * path.clone();
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_clip_layer(&on_path);
|
||||
picture(ctx, pres, &resolved(rctx, pf, ph), on_slide, Affine::IDENTITY, None);
|
||||
ctx.pop_layer();
|
||||
return;
|
||||
}
|
||||
ctx.push_clip_layer(path);
|
||||
picture(ctx, pres, &resolved(rctx, pf, ph), bounds, m, None);
|
||||
ctx.pop_layer();
|
||||
|
|
|
|||
|
|
@ -683,3 +683,30 @@ fn stars_and_right_arrows_have_sharp_corners_by_default() {
|
|||
assert!(close(img.pixel(95, 95), WHITE, 30), "{:?}", img.pixel(95, 95));
|
||||
assert!(deckcraft_geom::preset::mitred_by_default("rightArrow") && !deckcraft_geom::preset::mitred_by_default("leftArrow"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_upright_picture_fill_stays_upright_in_a_turned_shape() {
|
||||
use deckcraft_model::style::PictureFill;
|
||||
let blue = Rgba::rgb(0, 0, 255);
|
||||
for upright in [false, true] {
|
||||
let mut p = Presentation::default();
|
||||
let media = p.add_media("t.png", "image/png", half_red_half_blue());
|
||||
// Half red (left) half blue picture in a 200 × 200 square turned a quarter (and so
|
||||
// covering the same square).
|
||||
let sh = Shape {
|
||||
id: ShapeId(30),
|
||||
xfrm: Some(Xfrm { rot: 90.0, ..Xfrm::new(100.0, 100.0, 200.0, 200.0) }),
|
||||
fill: Some(Fill::Picture(PictureFill { media, upright, ..Default::default() })),
|
||||
line: no_line(),
|
||||
..Default::default()
|
||||
};
|
||||
std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh];
|
||||
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
let (left, top) = (img.pixel(110, 200), img.pixel(200, 110));
|
||||
if upright {
|
||||
assert!(close(left, RED, 30) && close(img.pixel(290, 200), blue, 30), "upright: red left {left:?}");
|
||||
} else {
|
||||
assert!(close(top, RED, 30), "turned with the shape: red at the top {top:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue