mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-09 03:49:34 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
921b686b3a | ||
|
|
0f28312bef |
@@ -148,19 +148,7 @@ pub fn rotate(dir: &Path) -> std::io::Result<(PathBuf, File)> {
|
||||
/// One formatted log line: `<timestamp> <LEVEL> [<thread>] <target>: <message>\n`.
|
||||
pub fn format_line(ts: &str, level: log::Level, thread: &str, target: &str, message: &str) -> String {
|
||||
let message = message.strip_suffix('\n').unwrap_or(message);
|
||||
// Messages can embed document-controlled text (a part name, a relationship id): control
|
||||
// characters other than tab are escaped and each continuation line is marked, so such text
|
||||
// cannot start a line that looks like a record of its own.
|
||||
let mut text = String::with_capacity(message.len());
|
||||
for c in message.chars() {
|
||||
match c {
|
||||
'\n' => text.push_str("\n | "),
|
||||
'\t' => text.push(c),
|
||||
c if c.is_control() => text.extend(c.escape_default()),
|
||||
c => text.push(c),
|
||||
}
|
||||
}
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {text}\n")
|
||||
format!("{ts} {level:<5} [{thread}] {target}: {message}\n")
|
||||
}
|
||||
|
||||
/// Where formatted lines go besides standard error: kept in memory until a file is attached,
|
||||
@@ -458,13 +446,8 @@ mod tests {
|
||||
),
|
||||
"2026-10-08T07:59:17.728Z WARN [main] deckcraft_pptx::opc: pptx: skipping oversized part ppt/media/image1.png\n"
|
||||
);
|
||||
// A multi-line message keeps its lines, each continuation marked; the record still ends in
|
||||
// exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\n | b\n"));
|
||||
// Injected line breaks and escape sequences cannot forge a record.
|
||||
let line = format_line("t", log::Level::Warn, "w", "x", "part a\r\n2026 ERROR [m] y: forged\x1b[2J");
|
||||
assert_eq!(line.matches('\n').count(), 2);
|
||||
assert!(!line.contains('\r') && !line.contains('\x1b'));
|
||||
// A multi-line message keeps its lines; the record still ends in exactly one newline.
|
||||
assert!(format_line("t", log::Level::Error, "w", "x", "a\nb\n").ends_with("x: a\nb\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -12,6 +12,9 @@ use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHand
|
||||
|
||||
use crate::icons::{self, Icon};
|
||||
|
||||
/// Video shapes that get a decoder at once; more play their sound without a picture.
|
||||
const MAX_VIDEO_FEEDS: usize = 16;
|
||||
|
||||
/// Who started a voice: the editor's control bar stops when you leave the slide, the slide show
|
||||
/// manages its own.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -227,12 +230,21 @@ impl MediaHost {
|
||||
audio: clip.volume > 0.0,
|
||||
};
|
||||
let source_changed = self.owners.get(&id).is_some_and(|o| o.source != media_key);
|
||||
if clip.video && (source_changed || !self.videos.contains_key(&id)) {
|
||||
let feed = match VideoFeed::new(bytes.clone()) {
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
log::info!("video {id}: {e}");
|
||||
None
|
||||
// A shape whose feed could not be had (the cap, a decode error) tries again on each play.
|
||||
let feedless = self.videos.get(&id).is_some_and(|v| v.feed.is_none());
|
||||
if clip.video && (source_changed || feedless || !self.videos.contains_key(&id)) {
|
||||
// One decoder and frame queue per video shape: past the cap a shape plays its sound only.
|
||||
let live = self.videos.iter().filter(|(vid, v)| **vid != id && v.feed.is_some()).count();
|
||||
let feed = if live >= MAX_VIDEO_FEEDS {
|
||||
log::info!("video {id}: more than {MAX_VIDEO_FEEDS} videos playing; showing no picture");
|
||||
None
|
||||
} else {
|
||||
match VideoFeed::new(bytes.clone()) {
|
||||
Ok(f) => Some(f),
|
||||
Err(e) => {
|
||||
log::info!("video {id}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
self.videos.insert(id, Video { feed, frame: None, tex: None });
|
||||
|
||||
@@ -196,18 +196,40 @@ pub(super) fn part_textures(ctx: &egui::Context, doc: &Presentation, from: Optio
|
||||
]
|
||||
}
|
||||
|
||||
/// Width of a slide-show grid thumbnail, in pixels.
|
||||
const THUMB_WIDTH: f64 = 320.0;
|
||||
/// Tallest grid thumbnail, so an extreme portrait slide cannot ask for a huge raster.
|
||||
const THUMB_MAX_HEIGHT: f64 = 2048.0;
|
||||
|
||||
/// The scale and pixel size of a grid thumbnail of a `width` x `height` slide.
|
||||
fn thumb_size(width: f64, height: f64) -> (f64, f64, f64) {
|
||||
let (sw, sh) = (width.max(1.0), height.max(1.0));
|
||||
let scale = (THUMB_WIDTH / sw).min(THUMB_MAX_HEIGHT / sh);
|
||||
(scale, (sw * scale).round().max(1.0), (sh * scale).round().max(1.0))
|
||||
}
|
||||
|
||||
pub(super) fn render_thumb(ctx: &egui::Context, show: &mut Show, doc: &Presentation, i: usize) -> Option<TextureHandle> {
|
||||
if show.thumbs.len() >= GRID_CAP {
|
||||
return None;
|
||||
}
|
||||
let w = 320.0_f64;
|
||||
let h = (w * doc.slide_size.height / doc.slide_size.width.max(1.0)).round().max(1.0);
|
||||
let img = deckcraft_render::render_slide(
|
||||
doc,
|
||||
i,
|
||||
&RenderOpts { scale: w / doc.slide_size.width.max(1.0), size: Some((w as u32, h as u32)), ..Default::default() },
|
||||
);
|
||||
// 320 wide, unless the slide is so tall that this would pass the height limit: fit that box.
|
||||
let (scale, w, h) = thumb_size(doc.slide_size.width, doc.slide_size.height);
|
||||
let img = deckcraft_render::render_slide(doc, i, &RenderOpts { scale, size: Some((w as u32, h as u32)), ..Default::default() });
|
||||
let tex = crate::textures::upload(ctx, format!("show-grid-{i}"), crate::textures::to_color_image(&img, false));
|
||||
show.thumbs.push((i, tex.clone()));
|
||||
Some(tex)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod thumb_size_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ordinary_slides_are_320_wide_and_extreme_ones_stay_in_the_height_limit() {
|
||||
let (scale, w, h) = thumb_size(960.0, 540.0);
|
||||
assert_eq!((w, h), (320.0, 180.0));
|
||||
assert!((scale - 320.0 / 960.0).abs() < 1e-12);
|
||||
let (_, w, h) = thumb_size(1.0, 1.0e9);
|
||||
assert!(h <= THUMB_MAX_HEIGHT && w >= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user