mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 02:27:08 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d819908a | ||
|
|
b913c4f9d0 | ||
|
|
9aef21e627 |
Generated
+404
-587
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -54,22 +54,22 @@ unicode-linebreak = "0.1"
|
||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
||||
|
||||
# text / rendering / formats
|
||||
skrifa = "0.47"
|
||||
harfrust = "0.13"
|
||||
vello_cpu = "0.2"
|
||||
vello_common = { version = "0.2", default-features = false }
|
||||
skrifa = "0.48"
|
||||
harfrust = "0.14"
|
||||
vello_cpu = "0.3"
|
||||
vello_common = { version = "0.3", default-features = false }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] }
|
||||
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "f0ad6c1e1d297908915f4be315b0fcfc9be7b5d4", version = "=0.1.0", default-features = false }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.1.0", default-features = false, features = ["write"] }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
|
||||
i_overlay = "9"
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
quick-xml = "0.42"
|
||||
zip = { version = "9", default-features = false, features = ["deflate"] }
|
||||
|
||||
# ui
|
||||
egui = "0.36"
|
||||
egui = "0.37"
|
||||
egui_extras = { version = "0.36", default-features = false }
|
||||
eframe = { version = "0.36", default-features = false }
|
||||
egui_kittest = "0.36"
|
||||
eframe = { version = "0.37", default-features = false }
|
||||
egui_kittest = "0.37"
|
||||
rfd = "0.17"
|
||||
arboard = { version = "3.6", default-features = false }
|
||||
|
||||
@@ -103,4 +103,4 @@ codegen-units = 16
|
||||
debug = 0
|
||||
|
||||
[patch.crates-io]
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.8.2" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.8.2" }
|
||||
|
||||
@@ -28,7 +28,7 @@ winresource = "0.1"
|
||||
rfd = { workspace = true }
|
||||
arboard = { workspace = true, features = ["image-data"] }
|
||||
image = { workspace = true }
|
||||
cpal = "0.16"
|
||||
cpal = "0.18"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# Finder double-click, Open With, Dock drops and `open -a DeckCraft deck.pptx` arrive as a
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,4 +15,4 @@ harfrust = { workspace = true }
|
||||
unicode-bidi = { workspace = true }
|
||||
log = { workspace = true }
|
||||
# Last-resort face only (Ubuntu Light); no UI code.
|
||||
epaint_default_fonts = "0.36"
|
||||
epaint_default_fonts = "0.37"
|
||||
|
||||
@@ -22,4 +22,4 @@ log = { workspace = true }
|
||||
image = { workspace = true }
|
||||
# Audio decoding for the formats we have no decoder of our own for (MPL-2.0, used unmodified,
|
||||
# as in FilmCraft).
|
||||
symphonia = { version = "0.5.5", default-features = false, features = ["aac", "adpcm", "alac", "flac", "mp1", "mp2", "mp3", "pcm", "vorbis", "wav", "aiff", "caf", "ogg", "mkv", "isomp4"] }
|
||||
symphonia = { version = "0.6.1", default-features = false, features = ["aac", "adpcm", "alac", "flac", "mp1", "mp2", "mp3", "pcm", "vorbis", "wav", "aiff", "caf", "ogg", "mkv", "isomp4"] }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hayro-syntax = { version = "=0.7.2", default-features = false, features = ["std"] }
|
||||
hayro-syntax = { version = "=0.8.0", default-features = false, features = ["std"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
@@ -29,4 +29,4 @@ wasm-bindgen-test = "0.3"
|
||||
# slide renders (the same bytes, in parallel). The root patch selects the same pinned
|
||||
# backend as craft-pdf; this dependency only enables its native compression feature.
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
krilla = { version = "=0.8.2", features = ["rayon"] }
|
||||
krilla = { version = "=0.8.2", default-features = false, features = ["rayon"] }
|
||||
|
||||
+11
-10
@@ -6,7 +6,7 @@ use std::process::Command;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
const SHARED_REV: &str = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8";
|
||||
const SHARED_REV: &str = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef";
|
||||
const REGISTRY: &str = "registry+https://github.com/rust-lang/crates.io-index";
|
||||
|
||||
pub fn run(root: &Path) -> Result<(), String> {
|
||||
@@ -72,8 +72,8 @@ fn validate(metadata: &Value) -> Result<(), String> {
|
||||
};
|
||||
let source = format!("git+https://github.com/storytold/craft-libs?rev={SHARED_REV}#{SHARED_REV}");
|
||||
for (name, version, expected_source, expected_features) in [
|
||||
("craft-pdf", "0.1.0", source.as_str(), vec!["write"]),
|
||||
("krilla", "0.8.2", source.as_str(), vec!["default", "raster-images", "rayon", "simple-text"]),
|
||||
("craft-pdf", "0.1.0", source.as_str(), vec!["write", "write-simple-text"]),
|
||||
("krilla", "0.8.2", source.as_str(), vec!["raster-images", "rayon", "simple-text"]),
|
||||
("hayro-syntax", "0.7.2", REGISTRY, vec!["std"]),
|
||||
] {
|
||||
let package = one_package(name)?;
|
||||
@@ -119,7 +119,7 @@ fn validate(metadata: &Value) -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn validate_target_features(tree: &str, parallel: bool) -> Result<(), String> {
|
||||
let mut expected = BTreeSet::from(["default", "raster-images", "simple-text"]);
|
||||
let mut expected = BTreeSet::from(["raster-images", "simple-text"]);
|
||||
if parallel {
|
||||
expected.insert("rayon");
|
||||
}
|
||||
@@ -153,8 +153,8 @@ mod tests {
|
||||
],
|
||||
"resolve":{"nodes":[
|
||||
{"id":"app", "deps":[{"pkg":"writer", "dep_kinds":[{"kind":null}]}, {"pkg":"parser", "dep_kinds":[{"kind":"dev"}]}]},
|
||||
{"id":"writer", "features":["write"], "deps":[{"pkg":"backend", "dep_kinds":[{"kind":null}]}]},
|
||||
{"id":"backend", "features":["default","raster-images","rayon","simple-text"], "deps":[]},
|
||||
{"id":"writer", "features":["write","write-simple-text"], "deps":[{"pkg":"backend", "dep_kinds":[{"kind":null}]}]},
|
||||
{"id":"backend", "features":["raster-images","rayon","simple-text"], "deps":[]},
|
||||
{"id":"parser", "features":["std"], "deps":[]}
|
||||
]}
|
||||
})
|
||||
@@ -162,8 +162,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parallel_compression_is_required_only_on_native_targets() {
|
||||
let serial = "krilla v0.8.2 (pinned)|default,raster-images,simple-text\n";
|
||||
let parallel = "krilla v0.8.2 (pinned)|default,raster-images,rayon,simple-text\n";
|
||||
let serial = "krilla v0.8.2 (pinned)|raster-images,simple-text\n";
|
||||
let parallel = "krilla v0.8.2 (pinned)|raster-images,rayon,simple-text\n";
|
||||
assert!(validate_target_features(serial, false).is_ok());
|
||||
assert!(validate_target_features(parallel, true).is_ok());
|
||||
assert!(validate_target_features(serial, true).is_err());
|
||||
@@ -189,8 +189,9 @@ mod tests {
|
||||
fn rejects_import_features_and_missing_text_shaping() {
|
||||
for (node, features) in [
|
||||
(1, json!([])),
|
||||
(1, json!(["write", "write-pdf"])),
|
||||
(2, json!(["default", "raster-images", "rayon", "simple-text", "pdf"])),
|
||||
(1, json!(["write"])),
|
||||
(1, json!(["write", "write-simple-text", "write-pdf"])),
|
||||
(2, json!(["raster-images", "rayon", "simple-text", "pdf"])),
|
||||
(2, json!(["raster-images"])),
|
||||
(3, json!(["std", "images"])),
|
||||
] {
|
||||
|
||||
Reference in New Issue
Block a user