fix: preserve fast gif frame timing
Build And Push Container / build-and-push (push) Successful in 56s
Build And Push Container / build-and-push (push) Successful in 56s
This commit is contained in:
@@ -15,6 +15,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub(crate) const NYAN_CAT_GIF: &[u8] = include_bytes!("../assets/nyan-cat.gif");
|
||||
pub(crate) const MIN_GIF_FRAME_DELAY_MS: u32 = 33;
|
||||
const GIF_FRAME_PREFIX: &str = ".gifframe-";
|
||||
const GIF_MANIFEST_SUFFIX: &str = ".gifset.json";
|
||||
|
||||
@@ -95,13 +96,25 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8])
|
||||
let base = make_image_stem(file_name);
|
||||
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
|
||||
let manifest_name = format!("panel-{timestamp}-{base}{GIF_MANIFEST_SUFFIX}");
|
||||
let mut stored_frames = Vec::new();
|
||||
let mut rendered_frames = Vec::new();
|
||||
|
||||
for (idx, frame) in frames.into_iter().enumerate() {
|
||||
for frame in frames {
|
||||
let delay_ms = frame_delay_ms(&frame);
|
||||
let rendered = render_display_panel(&DynamicImage::ImageRgba8(frame.into_buffer()));
|
||||
rendered_frames.push((rendered, delay_ms));
|
||||
}
|
||||
|
||||
let collapsed_frames = collapse_rendered_frames(rendered_frames);
|
||||
if collapsed_frames.len() < 2 {
|
||||
anyhow::bail!(
|
||||
"GIF contains no visible animation after decoding; please upload a truly animated GIF"
|
||||
);
|
||||
}
|
||||
|
||||
let mut stored_frames = Vec::new();
|
||||
for (idx, (rendered, delay_ms)) in collapsed_frames.into_iter().enumerate() {
|
||||
let frame_name = format!("{GIF_FRAME_PREFIX}{timestamp}-{base}-{idx:03}.jpg");
|
||||
let frame_path = state.image_dir.join(&frame_name);
|
||||
let rendered = render_display_panel(&DynamicImage::ImageRgba8(frame.into_buffer()));
|
||||
rendered
|
||||
.save_with_format(&frame_path, ImageFormat::Jpeg)
|
||||
.with_context(|| format!("Failed to save {}", frame_path.display()))?;
|
||||
@@ -128,6 +141,23 @@ async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8])
|
||||
Ok(manifest_name)
|
||||
}
|
||||
|
||||
fn collapse_rendered_frames(frames: Vec<(RgbImage, u32)>) -> Vec<(RgbImage, u32)> {
|
||||
let mut collapsed: Vec<(RgbImage, u32)> = Vec::new();
|
||||
|
||||
for (frame, delay_ms) in frames {
|
||||
if let Some((prev_frame, prev_delay_ms)) = collapsed.last_mut()
|
||||
&& *prev_frame == frame
|
||||
{
|
||||
*prev_delay_ms = prev_delay_ms.saturating_add(delay_ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
collapsed.push((frame, delay_ms));
|
||||
}
|
||||
|
||||
collapsed
|
||||
}
|
||||
|
||||
pub(crate) fn render_display_panel(image: &DynamicImage) -> RgbImage {
|
||||
let resized = image.resize(DISPLAY_WIDTH, DISPLAY_HEIGHT, FilterType::Lanczos3);
|
||||
let rgb = resized.to_rgb8();
|
||||
@@ -187,7 +217,7 @@ fn frame_delay_ms(frame: &Frame) -> u32 {
|
||||
} else {
|
||||
((numer as f64) / (denom as f64)).round() as u32
|
||||
};
|
||||
delay.max(100)
|
||||
delay.max(MIN_GIF_FRAME_DELAY_MS)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_image_asset(state: &AppState, name: &str) -> Result<()> {
|
||||
@@ -228,3 +258,32 @@ pub(crate) fn load_panel_rgb(image_dir: &PathBuf, image_name: &str) -> Result<Rg
|
||||
.with_context(|| format!("Failed to load panel image {}", path.display()))?;
|
||||
Ok(image.to_rgb8())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use image::{Rgb, RgbImage};
|
||||
|
||||
use super::{collapse_rendered_frames, MIN_GIF_FRAME_DELAY_MS};
|
||||
|
||||
#[test]
|
||||
fn collapse_rendered_frames_merges_identical_neighbors() {
|
||||
let frame_a = RgbImage::from_pixel(2, 2, Rgb([1, 2, 3]));
|
||||
let frame_b = RgbImage::from_pixel(2, 2, Rgb([9, 8, 7]));
|
||||
|
||||
let collapsed = collapse_rendered_frames(vec![
|
||||
(frame_a.clone(), 100),
|
||||
(frame_a, 120),
|
||||
(frame_b.clone(), 80),
|
||||
(frame_b, 90),
|
||||
]);
|
||||
|
||||
assert_eq!(collapsed.len(), 2);
|
||||
assert_eq!(collapsed[0].1, 220);
|
||||
assert_eq!(collapsed[1].1, 170);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_gif_frame_delay_supports_50ms_frames() {
|
||||
assert!(MIN_GIF_FRAME_DELAY_MS <= 50);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user