mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 02:27:08 -06:00
Merge pull request #1008 from MarekSpodBiedry/issue-972
# Conflicts: # crates/ui-egui/src/show.rs
This commit is contained in:
@@ -498,8 +498,9 @@ fn sane_shapes(shapes: &mut [deckcraft_model::Shape], depth: usize) {
|
||||
}
|
||||
}
|
||||
match &mut sh.kind {
|
||||
deckcraft_model::ShapeKind::Picture { fill } => {
|
||||
for c in &mut fill.crop {
|
||||
deckcraft_model::ShapeKind::Picture { fill: deckcraft_model::style::PictureFill { crop, .. } }
|
||||
| deckcraft_model::ShapeKind::Media(deckcraft_model::MediaClip { crop, .. }) => {
|
||||
for c in crop {
|
||||
*c = if c.is_finite() { c.clamp(-10.0, 1.0) } else { 0.0 };
|
||||
}
|
||||
}
|
||||
@@ -827,6 +828,18 @@ mod tests {
|
||||
assert!(load(&b).is_err());
|
||||
}
|
||||
|
||||
/// A video's crop is brought into the same range as a picture's (#972).
|
||||
#[test]
|
||||
fn a_video_crop_is_repaired_like_a_picture_crop() {
|
||||
use deckcraft_model::{MediaClip, Shape};
|
||||
let mut p = Presentation::default();
|
||||
let clip = MediaClip { video: true, crop: [f64::NAN, 0.25, -1e9, 0.0], ..Default::default() };
|
||||
Arc::make_mut(&mut p.slides[0]).shapes.push(Shape { kind: ShapeKind::Media(clip), ..Default::default() });
|
||||
let back = load(&save(&p).unwrap()).unwrap_or_else(|e| panic!("{e}"));
|
||||
let ShapeKind::Media(m) = &back.slides[0].shapes.last().unwrap().kind else { panic!("media") };
|
||||
assert_eq!(m.crop, [0.0, 0.25, -10.0, 0.0]);
|
||||
}
|
||||
|
||||
/// Non-finite numbers are written as `null` by serde_json; the file still opens, with each
|
||||
/// such number repaired (it failed as "invalid type: null, expected f64").
|
||||
#[test]
|
||||
|
||||
@@ -231,6 +231,8 @@ pub struct MediaClip {
|
||||
pub video: bool,
|
||||
/// Poster frame image.
|
||||
pub poster: Option<MediaId>,
|
||||
/// Crop of the picture (left, top, right, bottom as fractions), PowerPoint's `srcRect` on the video.
|
||||
pub crop: [f64; 4],
|
||||
pub trim_start_ms: u32,
|
||||
pub trim_end_ms: u32,
|
||||
pub fade_in_ms: u32,
|
||||
|
||||
@@ -347,8 +347,14 @@ fn pic(imp: &mut Imp, part: &Part, ctx: &mut IdCtx, e: &El) -> Shape {
|
||||
if vf.is_some() || af.is_some() || p14.is_some() {
|
||||
let media = p14.and_then(|m| dml::media_ref(imp, part, m)).or_else(|| vf.or(af).and_then(|f| dml::media_ref(imp, part, f)));
|
||||
if let Some(media) = media {
|
||||
let mut clip =
|
||||
MediaClip { media, video: vf.is_some() || af.is_none(), poster: pf.as_ref().map(|p| p.media), volume: 1.0, ..Default::default() };
|
||||
let mut clip = MediaClip {
|
||||
media,
|
||||
video: vf.is_some() || af.is_none(),
|
||||
poster: pf.as_ref().map(|p| p.media),
|
||||
crop: pf.as_ref().map(|p| p.crop).unwrap_or_default(),
|
||||
volume: 1.0,
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(m) = p14 {
|
||||
if let Some(t) = m.child("trim") {
|
||||
clip.trim_start_ms = ms(t.attr("st"));
|
||||
|
||||
@@ -828,7 +828,7 @@ fn media(w: &mut W, x: &mut Exp, o: &mut Out, id: u32, s: &Shape, m: &deckcraft_
|
||||
}
|
||||
w.close("p:nvPr");
|
||||
w.close("p:nvPicPr");
|
||||
let poster = deckcraft_model::style::PictureFill { media: m.poster.unwrap_or_default(), ..Default::default() };
|
||||
let poster = deckcraft_model::style::PictureFill { media: m.poster.unwrap_or_default(), crop: m.crop, ..Default::default() };
|
||||
dml::blip_fill(w, x, o, "p:blipFill", &poster);
|
||||
let mut s3 = s.clone();
|
||||
s3.fill = None;
|
||||
|
||||
@@ -256,6 +256,7 @@ pub fn rich_deck() -> Presentation {
|
||||
media: video,
|
||||
video: true,
|
||||
poster: Some(img),
|
||||
crop: [0.1, 0.0, 0.2, 0.25],
|
||||
volume: 1.0,
|
||||
trim_start_ms: 500,
|
||||
fade_in_ms: 250,
|
||||
@@ -505,6 +506,10 @@ fn pictures_media_and_ink_round_trip() {
|
||||
assert_eq!((m.trim_start_ms, m.fade_in_ms), (500, 250));
|
||||
assert!(q.media(m.media).is_some_and(|x| x.data.starts_with(b"\x00\x00\x00\x18ftyp")));
|
||||
assert!(m.poster.is_some());
|
||||
// The crop of the video survives the round trip (#972).
|
||||
for (got, want) in m.crop.iter().zip([0.1, 0.0, 0.2, 0.25]) {
|
||||
assert!((got - want).abs() < 1e-4, "video crop {:?}", m.crop);
|
||||
}
|
||||
// Ink comes back as freeform lines in a group. A flat 0.5 stroke keeps the pen width.
|
||||
let ink = by_name(&q.slides[2], "Ink");
|
||||
let ShapeKind::Group { children, .. } = &ink.kind else { panic!("ink is not a group: {:?}", ink.kind) };
|
||||
|
||||
@@ -1138,8 +1138,10 @@ impl Renderer {
|
||||
ctx.set_transform(m);
|
||||
let bounds = Rect::new(0.0, 0.0, w, h);
|
||||
if let Some(p) = mc.poster {
|
||||
let pf = deckcraft_model::style::PictureFill { media: p, ..Default::default() };
|
||||
let pf = deckcraft_model::style::PictureFill { media: p, crop: mc.crop, ..Default::default() };
|
||||
ctx.push_clip_layer(&outline);
|
||||
paint::picture(ctx, f.pres, &pf, bounds, m, None);
|
||||
ctx.pop_layer();
|
||||
} else if mc.video {
|
||||
ctx.set_paint(peniko::Color::from_rgba8(20, 20, 24, 255));
|
||||
ctx.fill_rect(&bounds);
|
||||
|
||||
@@ -507,7 +507,7 @@ fn media_frames(app: &SlideApp, painter: &egui::Painter, xf: Xf) {
|
||||
}
|
||||
for (id, x, clip) in crate::media::slide_media(&d.doc, d.selection.slide) {
|
||||
if clip.video {
|
||||
crate::media::paint_frame(painter, &app.media, id, screen_rect(xf, &x));
|
||||
crate::media::paint_frame(painter, &app.media, id, screen_rect(xf, &x), clip.crop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,9 +405,21 @@ pub fn clock(secs: f64) -> String {
|
||||
format!("{:02}:{:05.2}", (s / 60.0) as u32, s % 60.0)
|
||||
}
|
||||
|
||||
/// Draw the playing video frame of `id` into `rect` (letterboxed to the frame's aspect).
|
||||
pub fn paint_frame(painter: &egui::Painter, host: &MediaHost, id: ShapeId, rect: Rect) -> bool {
|
||||
/// The part of a video frame a picture crop (left, top, right, bottom fractions) leaves visible.
|
||||
fn crop_uv(crop: [f64; 4]) -> Rect {
|
||||
let [l, t, r, b] = crop.map(|v| if v.is_finite() { v.clamp(0.0, 0.99) as f32 } else { 0.0 });
|
||||
Rect::from_min_max(pos2(l, t), pos2((1.0 - r).max(l + 0.01), (1.0 - b).max(t + 0.01)))
|
||||
}
|
||||
|
||||
/// Draw the playing video frame of `id` into `rect`: letterboxed to the frame's aspect, or, when
|
||||
/// the video is cropped, the cropped part stretched over the whole box as PowerPoint shows it.
|
||||
pub fn paint_frame(painter: &egui::Painter, host: &MediaHost, id: ShapeId, rect: Rect, crop: [f64; 4]) -> bool {
|
||||
let Some(tex) = host.texture(id) else { return false };
|
||||
if crop.iter().any(|v| *v != 0.0) {
|
||||
painter.rect_filled(rect, CornerRadius::ZERO, Color32::BLACK);
|
||||
painter.image(tex.id(), rect, crop_uv(crop), Color32::WHITE);
|
||||
return true;
|
||||
}
|
||||
let [w, h] = tex.size();
|
||||
let (w, h) = (w.max(1) as f32, h.max(1) as f32);
|
||||
let k = (rect.width() / w).min(rect.height() / h);
|
||||
@@ -554,6 +566,12 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_video_crop_shows_only_the_cropped_part_of_the_frame() {
|
||||
assert_eq!(crop_uv([0.0; 4]), Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)));
|
||||
assert_eq!(crop_uv([0.25, 0.5, 0.25, 0.0]), Rect::from_min_max(pos2(0.25, 0.5), pos2(0.75, 1.0)));
|
||||
}
|
||||
|
||||
/// 16-bit mono WAV of a constant level, `secs` long at 8 kHz.
|
||||
fn wav_level(secs: f64, amp: f32) -> Vec<u8> {
|
||||
let rate = 8_000u32;
|
||||
|
||||
@@ -999,7 +999,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
} else {
|
||||
slide_box(srect, uv, sw, sh, x.x as f32, x.y as f32, x.w as f32, x.h as f32)
|
||||
};
|
||||
if crate::media::paint_frame(&painter, &app.media, *id, r) {
|
||||
if crate::media::paint_frame(&painter, &app.media, *id, r, clip.crop) {
|
||||
framed.push(*id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user