mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
//! File menu: new, open, save, export, close, properties.
|
//! 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 serde_json::{Value, json};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -156,6 +160,7 @@ fn open_other(name: &str, bytes: &[u8]) -> Result<(Presentation, Vec<String>)> {
|
|||||||
if deckcraft_pptx::sniff(bytes) {
|
if deckcraft_pptx::sniff(bytes) {
|
||||||
let (mut p, lost_slides) = deckcraft_pptx::import_checked(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
|
let (mut p, lost_slides) = deckcraft_pptx::import_checked(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||||
deckcraft_format::repair(&mut p);
|
deckcraft_format::repair(&mut p);
|
||||||
|
probe_clips(&mut p);
|
||||||
return Ok((p, lost_slides));
|
return Ok((p, lost_slides));
|
||||||
}
|
}
|
||||||
if deckcraft_pptx::is_encrypted(bytes) {
|
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")))
|
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 {
|
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());
|
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);
|
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");
|
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]
|
#[test]
|
||||||
fn media_commands_need_media() {
|
fn media_commands_need_media() {
|
||||||
let mut s = Session::with_new();
|
let mut s = Session::with_new();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub use audio::Pcm;
|
|||||||
pub use clip::ClipParams;
|
pub use clip::ClipParams;
|
||||||
pub use feed::VideoFeed;
|
pub use feed::VideoFeed;
|
||||||
pub use player::{AudioOut, PlayState, Player, VoiceSpec, VoiceStatus};
|
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};
|
pub use video::{Frame, VideoDecoder, poster_frame, poster_png};
|
||||||
|
|
||||||
/// Shared media bytes (the model's `MediaItem::data`).
|
/// Shared media bytes (the model's `MediaItem::data`).
|
||||||
|
|||||||
@@ -48,6 +48,19 @@ fn ms(secs: f64) -> u64 {
|
|||||||
|
|
||||||
/// Probe `bytes`.
|
/// Probe `bytes`.
|
||||||
pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
|
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 c = Container::sniff(bytes);
|
||||||
let mut info = match c {
|
let mut info = match c {
|
||||||
Container::Mp4 => probe_mp4(bytes)?,
|
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);
|
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)
|
Ok(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user