diff --git a/crates/compositor-view-napi/src/lib.rs b/crates/compositor-view-napi/src/lib.rs index 78d351706..ed98a7104 100644 --- a/crates/compositor-view-napi/src/lib.rs +++ b/crates/compositor-view-napi/src/lib.rs @@ -759,7 +759,7 @@ pub struct GifParamsInput { pub width: Option, pub height: Option, pub fps: Option, - /// Compteur de loop GIF : `None` ou `0` = infini, sinon `n` boucles. + /// Nombre de lectures du GIF : `None` ou `0` = en boucle, `1` = une fois, `n` = n fois. pub loop_count: Option, /// Floyd-Steinberg error diffusion à la quantification. Actif par défaut : /// sans lui, un fond en dégradé se découpe en bandes (cf. `GifExportParams`). diff --git a/crates/compositor/src/gif_export.rs b/crates/compositor/src/gif_export.rs index 5a8c13388..54293a176 100644 --- a/crates/compositor/src/gif_export.rs +++ b/crates/compositor/src/gif_export.rs @@ -132,8 +132,8 @@ pub struct GifExportParams { pub width: Option, pub height: Option, pub fps: Option, - /// `None` or `0` → infinite loop (the historical GIF default). - /// Otherwise finite count. + /// How many times the animation plays: `None` or `0` → forever (the + /// historical GIF default), `1` → once, `n` → n times. pub loop_count: Option, /// Floyd-Steinberg dithering at quantization. Default ON: the median /// cut spreads its 256 entries over the whole frame, so a gradient @@ -156,6 +156,18 @@ impl Default for GifExportParams { } } +/// The count to write in the Netscape 2.0 loop block for `loop_count` plays, +/// or `None` for no block. Viewers read the block's count as repeats after +/// the first play (Chromium, Firefox, ffmpeg's `-loop`), and 0 as forever, so +/// a GIF that plays once must carry no block at all: a count of 1 plays twice. +fn netscape_repeats(loop_count: Option) -> Option { + match loop_count { + None | Some(0) => Some(0), + Some(1) => None, + Some(plays) => Some(plays - 1), + } +} + /// Drive a multiclip GIF export end-to-end. /// /// Deliberately the same shape as `pipeline::run_composited_multi`, and @@ -238,14 +250,9 @@ fn export_gif_inner( let frames = { let mut gw = GifWriter::new(&mut writer, width as u16, height as u16)?; gw.write_header()?; - // Netscape 2.0 application extension drives the loop count. - // 0 = infinite; some viewers also treat 0 as infinite, so we - // keep that as the "default". - let loops = match params.loop_count { - None | Some(0) => 0u16, - Some(n) => n, - }; - gw.write_netscape_loop(loops)?; + if let Some(repeats) = netscape_repeats(params.loop_count) { + gw.write_netscape_loop(repeats)?; + } let mut screen_decs: HashMap = HashMap::new(); let mut webcam_decs: HashMap = HashMap::new(); @@ -1124,6 +1131,14 @@ mod tests { use super::*; use std::io::Cursor; + #[test] + fn a_gif_that_plays_once_carries_no_loop_block() { + assert_eq!(netscape_repeats(None), Some(0)); + assert_eq!(netscape_repeats(Some(0)), Some(0)); + assert_eq!(netscape_repeats(Some(1)), None); + assert_eq!(netscape_repeats(Some(3)), Some(2)); + } + #[test] fn frame_delays_sum_to_programme_duration() { for fps in [15u32, 30, 12, 20, 25, 24, 60] { diff --git a/electron/native/compositor-view/addon.d.ts b/electron/native/compositor-view/addon.d.ts index ade2769a7..5a285bb4e 100644 --- a/electron/native/compositor-view/addon.d.ts +++ b/electron/native/compositor-view/addon.d.ts @@ -98,7 +98,7 @@ export interface GifParamsInput { width?: number; height?: number; fps?: number; - /** GIF loop count: `0` or omitted = infinite, else `n` finite loops. */ + /** How many times the GIF plays: `0` or omitted = forever, `1` = once, `n` = n times. */ loopCount?: number; /** Floyd-Steinberg error diffusion at quantization. On by default. */ dither?: boolean; diff --git a/src/native/contracts.ts b/src/native/contracts.ts index 7576b2cc7..38bfaf1df 100644 --- a/src/native/contracts.ts +++ b/src/native/contracts.ts @@ -240,7 +240,7 @@ export interface CompositorExportGifParams { width?: number; height?: number; fps?: number; - /** Compteur de loop GIF : `0` ou omis = infini, sinon `n` boucles finies. */ + /** Nombre de lectures du GIF : `0` ou omis = en boucle, `1` = une fois, `n` = n fois. */ loopCount?: number; /** Floyd-Steinberg error diffusion à la quantification. `true` par défaut. */ dither?: boolean;