mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 02:27:08 -06:00
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
//! File menu: new, open, save, export, close, properties.
|
||||
|
||||
use deckcraft_model::{Presentation, defaults};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_media::MediaInfo;
|
||||
use deckcraft_model::{MediaId, MediaItem, Presentation, Shape, ShapeKind, defaults};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
@@ -156,6 +160,7 @@ fn open_other(name: &str, bytes: &[u8]) -> Result<(Presentation, Vec<String>)> {
|
||||
if deckcraft_pptx::sniff(bytes) {
|
||||
let (mut p, lost_slides) = deckcraft_pptx::import_checked(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
deckcraft_format::repair(&mut p);
|
||||
probe_clips(&mut p);
|
||||
return Ok((p, lost_slides));
|
||||
}
|
||||
if deckcraft_pptx::is_encrypted(bytes) {
|
||||
@@ -178,6 +183,42 @@ fn open_other(name: &str, bytes: &[u8]) -> Result<(Presentation, Vec<String>)> {
|
||||
Err(EngineError::Other(format!("{name}: not a presentation DeckCraft can read")))
|
||||
}
|
||||
|
||||
/// Fill in each audio and video clip's length and picture size from its bytes: a `.pptx` doesn't
|
||||
/// store them. Headers only, so opening never decodes a whole audio file.
|
||||
fn probe_clips(p: &mut Presentation) {
|
||||
fn rec(shapes: &mut [Shape], media: &[MediaItem], probed: &mut HashMap<MediaId, Option<MediaInfo>>, depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
for s in shapes {
|
||||
if let ShapeKind::Media(m) = &mut s.kind
|
||||
&& (m.duration_ms == 0 || m.width == 0 && m.height == 0)
|
||||
&& let Some(info) = probed
|
||||
.entry(m.media)
|
||||
.or_insert_with(|| media.iter().find(|i| i.id == m.media).and_then(|i| deckcraft_media::probe_headers(&i.data).ok()))
|
||||
{
|
||||
if m.duration_ms == 0 {
|
||||
m.duration_ms = info.duration_ms.min(u32::MAX as u64) as u32;
|
||||
}
|
||||
if m.width == 0
|
||||
&& m.height == 0
|
||||
&& let Some(v) = &info.video
|
||||
{
|
||||
(m.width, m.height) = (v.width, v.height);
|
||||
}
|
||||
}
|
||||
if let Some(children) = s.children_mut() {
|
||||
rec(children, media, probed, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut probed = HashMap::new();
|
||||
// Just read, so no slide is shared yet and `make_mut` doesn't copy.
|
||||
for slide in &mut p.slides {
|
||||
rec(&mut Arc::make_mut(slide).shapes, &p.media, &mut probed, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_opened(s: &mut Session, name: &str, path: Option<String>, p: Presentation) -> usize {
|
||||
let title = std::path::Path::new(name).file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| name.to_string());
|
||||
crate::fonts::install_embedded_fonts(&p);
|
||||
|
||||
@@ -228,6 +228,21 @@ mod tests {
|
||||
assert!(s.execute("media.posterFrame", &json!({})).is_err(), "audio has no poster frame");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_video_reopened_from_pptx_keeps_its_length_and_size() {
|
||||
let mut s = Session::with_new();
|
||||
let clip = include_bytes!("../../../media/tests/fixtures/clip-h264.mp4");
|
||||
s.execute("insert.video", &json!({"name": "clip.mp4", "data": base64_encode(clip)})).expect("insert");
|
||||
let before = s.execute("media.info", &json!({})).expect("info");
|
||||
assert!(before["durationMs"].as_u64().is_some_and(|ms| ms > 0));
|
||||
let saved = s.execute("file.saveBytes", &json!({"format": "pptx"})).expect("save");
|
||||
s.execute("file.openBytes", &json!({"name": "deck.pptx", "data": saved["data"]})).expect("open");
|
||||
let after = s.execute("media.info", &json!({})).expect("info");
|
||||
for key in ["durationMs", "width", "height"] {
|
||||
assert_eq!(after[key], before[key], "{key}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_commands_need_media() {
|
||||
let mut s = Session::with_new();
|
||||
|
||||
@@ -31,7 +31,7 @@ pub use audio::Pcm;
|
||||
pub use clip::ClipParams;
|
||||
pub use feed::VideoFeed;
|
||||
pub use player::{AudioOut, PlayState, Player, VoiceSpec, VoiceStatus};
|
||||
pub use probe::{AudioInfo, MediaInfo, VideoInfo, probe};
|
||||
pub use probe::{AudioInfo, MediaInfo, VideoInfo, probe, probe_headers};
|
||||
pub use video::{Frame, VideoDecoder, poster_frame, poster_png};
|
||||
|
||||
/// Shared media bytes (the model's `MediaItem::data`).
|
||||
|
||||
@@ -48,6 +48,19 @@ fn ms(secs: f64) -> u64 {
|
||||
|
||||
/// Probe `bytes`.
|
||||
pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
let mut info = probe_headers(bytes)?;
|
||||
if info.duration_ms == 0 && info.audio.as_ref().is_some_and(|a| a.decodable) && info.video.is_none() {
|
||||
// No length in the header (raw MP3 / ADTS without an index): decode to measure.
|
||||
if let Ok(pcm) = crate::audio::decode(bytes) {
|
||||
info.duration_ms = ms(pcm.duration());
|
||||
}
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// [`probe`] from the headers alone, without decoding: a raw MP3 or ADTS stream without an
|
||||
/// index reports a `duration_ms` of 0.
|
||||
pub fn probe_headers(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
let c = Container::sniff(bytes);
|
||||
let mut info = match c {
|
||||
Container::Mp4 => probe_mp4(bytes)?,
|
||||
@@ -73,12 +86,6 @@ pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
info.duration_ms = ms(n as f64 / rate as f64);
|
||||
}
|
||||
}
|
||||
if info.duration_ms == 0 && info.audio.as_ref().is_some_and(|a| a.decodable) && info.video.is_none() {
|
||||
// No length in the header (raw MP3 / ADTS without an index): decode to measure.
|
||||
if let Ok(pcm) = crate::audio::decode(bytes) {
|
||||
info.duration_ms = ms(pcm.duration());
|
||||
}
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user