Render: a picture's border lies outside the picture
DeckCraft centred a picture's outline on its edge, covering half the border's width of the picture. PowerPoint draws picture borders wholly outside: an 18 pt white photo frame leaves the whole photo showing and reaches 18 pt out (measured on its export; a 2 pt border on a cropped picture covers 18-20 pt for a picture starting at 20). Pictures now stroke their border twice as wide, clipped to outside the outline, keeping dash lengths; the border's shadow is cast from there too. Compound borders stay centred. Wild synthetic set: picture frames 13.12 -> 8.54, cropped/flipped pictures with borders 5.79 -> 2.57, crop-to-shape 8.03 -> 5.25. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
862e2175a7
commit
7d642e12c9
|
|
@ -864,7 +864,9 @@ impl Renderer {
|
|||
_ => 1.0,
|
||||
};
|
||||
let (base, rel) = self.line_space(m, depth);
|
||||
Caster { line: &line, ph: line_ph, alpha: a, base, rel }
|
||||
let lw = line.width.unwrap_or(0.75).max(0.0);
|
||||
let outside = if is_pic { outside_line(&line, lw) } else { None };
|
||||
Caster { line: outside.map_or(std::borrow::Cow::Borrowed(&line), std::borrow::Cow::Owned), ph: line_ph, alpha: a, base, rel }
|
||||
});
|
||||
self.effects_below(ctx, rctx, e, fx_ph, &geo, &fill, fill_alpha, outline, content.as_ref(), m, w, h);
|
||||
}
|
||||
|
|
@ -973,6 +975,22 @@ impl Renderer {
|
|||
}
|
||||
None => std::borrow::Cow::Borrowed(&line),
|
||||
};
|
||||
// A picture's border lies outside the picture, as PowerPoint draws it: stroked twice
|
||||
// as wide, outside the outline only.
|
||||
let outside = is_pic.then(|| outside_line(&line, lw)).flatten();
|
||||
let clipped = outside.is_some();
|
||||
let (line, lw) = match outside {
|
||||
Some(o) => {
|
||||
let mut clip = Rect::new(0.0, 0.0, w, h).inflate(w + h + lw * 8.0, w + h + lw * 8.0).to_path(0.1);
|
||||
clip.extend(outline.iter());
|
||||
ctx.set_transform(m);
|
||||
ctx.set_fill_rule(peniko::Fill::EvenOdd);
|
||||
ctx.push_clip_layer(&clip);
|
||||
ctx.set_fill_rule(peniko::Fill::NonZero);
|
||||
(std::borrow::Cow::Owned(o), lw * 2.0)
|
||||
}
|
||||
None => (line, lw),
|
||||
};
|
||||
let first_stroked = geo.paths.iter().position(|p| p.stroke);
|
||||
for (i, sp) in geo.paths.iter().enumerate().filter(|(i, p)| p.stroke && fade(*i).stroke > 0.0) {
|
||||
let mut path = if rel == Affine::IDENTITY {
|
||||
|
|
@ -991,6 +1009,9 @@ impl Renderer {
|
|||
if ends {
|
||||
arrowheads(ctx, rctx, &line, line_ph, &geo, rel, base, lw);
|
||||
}
|
||||
if clipped {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
if see_through.is_some() {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
|
|
@ -1153,7 +1174,7 @@ impl Renderer {
|
|||
}
|
||||
// … and the outline (a picture's border too).
|
||||
if let Some(o) = &outline {
|
||||
let cast = Line { fill: Some(Fill::solid(deckcraft_model::style::ColorRef::rgb(c.with_alpha(255)))), ..o.line.clone() };
|
||||
let cast = Line { fill: Some(Fill::solid(deckcraft_model::style::ColorRef::rgb(c.with_alpha(255)))), ..o.line.as_ref().clone() };
|
||||
let lw = o.line.width.unwrap_or(0.75).max(0.0);
|
||||
for sp in geo.paths.iter().filter(|sp| sp.stroke) {
|
||||
let mut path = sp.path.clone();
|
||||
|
|
@ -1197,10 +1218,26 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
/// The line that draws `line` (`lw` wide) outside a picture: twice as wide, so the half outside
|
||||
/// the outline is the border, with dashes kept their length. `None` for compound lines, whose
|
||||
/// bands would not survive the doubling (they stay centred on the outline).
|
||||
fn outside_line(line: &Line, lw: f64) -> Option<Line> {
|
||||
if line.compound.is_some_and(|c| c != deckcraft_model::style::Compound::Single) {
|
||||
return None;
|
||||
}
|
||||
let dash = line.dash.as_ref().map(|d| {
|
||||
let p = d.pattern();
|
||||
deckcraft_model::style::Dash::Custom {
|
||||
pattern: p.chunks(2).map(|c| (c.first().copied().unwrap_or(0.0) / 2.0, c.get(1).copied().unwrap_or(0.0) / 2.0)).collect(),
|
||||
}
|
||||
});
|
||||
Some(Line { width: Some(lw * 2.0), dash: dash.or_else(|| line.dash.clone()), ..line.clone() })
|
||||
}
|
||||
|
||||
/// A shape's outline as it casts a shadow: its line, that line's placeholder colour and opacity,
|
||||
/// and the space it is stroked in (see `Renderer::line_space`).
|
||||
struct Caster<'a> {
|
||||
line: &'a Line,
|
||||
line: std::borrow::Cow<'a, Line>,
|
||||
ph: Option<Rgba>,
|
||||
alpha: f64,
|
||||
base: Affine,
|
||||
|
|
|
|||
|
|
@ -250,3 +250,40 @@ fn shadows_are_cast_by_what_is_drawn() {
|
|||
let img = render(vec![ln]);
|
||||
assert!(close(img.pixel(310, 100), Rgba::BLACK, 30), "line shadow {:?}", img.pixel(310, 100));
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
|
||||
let mut out = std::io::Cursor::new(vec![]);
|
||||
image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(w, h, image::Rgb(rgb))).write_to(&mut out, image::ImageFormat::Png).unwrap();
|
||||
out.into_inner()
|
||||
}
|
||||
|
||||
fn picture_deck(line: Option<Line>) -> Presentation {
|
||||
let mut p = Presentation::default();
|
||||
let media = p.add_media("image1.png", "image/png", png(8, 8, [0, 0, 255]));
|
||||
let sh = Shape {
|
||||
id: ShapeId(15),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)),
|
||||
kind: ShapeKind::Picture { fill: deckcraft_model::style::PictureFill { media, ..Default::default() } },
|
||||
line,
|
||||
..Default::default()
|
||||
};
|
||||
std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh];
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pictures_border_lies_outside_the_picture() {
|
||||
let p = picture_deck(Some(Line::solid(ColorRef::rgb(RED), 8.0)));
|
||||
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
let blue = Rgba::rgb(0, 0, 255);
|
||||
// The whole picture shows, up to its edge; the 8 pt border is all outside it.
|
||||
assert!(close(img.pixel(101, 150), blue, 30), "picture at its edge {:?}", img.pixel(101, 150));
|
||||
assert!(close(img.pixel(98, 150), RED, 30) && close(img.pixel(93, 150), RED, 30), "{:?}", img.pixel(93, 150));
|
||||
assert!(close(img.pixel(90, 150), WHITE, 10), "border no wider than the line {:?}", img.pixel(90, 150));
|
||||
// A dashed border keeps its dash lengths (a dash is 4 widths: 32 pt), drawn from the corner.
|
||||
let mut line = Line::solid(ColorRef::rgb(RED), 8.0);
|
||||
line.dash = Some(deckcraft_model::style::Dash::Preset { name: "dash".into() });
|
||||
let img = render_slide(&picture_deck(Some(line)), 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
assert!(close(img.pixel(120, 96), RED, 30) && close(img.pixel(140, 96), WHITE, 10), "{:?}", img.pixel(140, 96));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue