Skip to main content

figif_core/
segment.rs

1//! Segment operations and manipulation.
2
3use crate::types::{
4    AnalyzedFrame, EncodableFrame, FrameOp, FrameOps, Segment, SegmentOp, SegmentOps,
5};
6
7#[cfg(feature = "parallel")]
8use rayon::prelude::*;
9
10/// Apply segment operations to analyzed frames, producing encodable frames.
11///
12/// This function takes the analyzed frames and a map of operations to apply
13/// to each segment, and produces a list of frames ready for encoding.
14///
15/// # Arguments
16///
17/// * `frames` - The analyzed frames
18/// * `segments` - The detected segments
19/// * `ops` - Operations to apply to each segment (by segment ID)
20///
21/// # Returns
22///
23/// A vector of encodable frames with timing adjustments applied.
24pub fn apply_segment_operations<H: Sync + Send>(
25    frames: &[AnalyzedFrame<H>],
26    segments: &[Segment],
27    ops: &SegmentOps,
28) -> Vec<EncodableFrame> {
29    #[cfg(feature = "parallel")]
30    {
31        segments
32            .par_iter()
33            .map(|segment| {
34                let op = ops.get(&segment.id).unwrap_or(&SegmentOp::Keep);
35                let segment_frames = &frames[segment.frame_range.clone()];
36                let mut segment_output = Vec::new();
37
38                match op {
39                    SegmentOp::Keep => {
40                        for frame in segment_frames {
41                            segment_output.push(EncodableFrame::from_decoded(&frame.frame));
42                        }
43                    }
44                    SegmentOp::Remove => {}
45                    SegmentOp::Collapse { delay_cs } => {
46                        if let Some(first) = segment_frames.first() {
47                            segment_output
48                                .push(EncodableFrame::new(first.frame.image.clone(), *delay_cs));
49                        }
50                    }
51                    SegmentOp::SetDuration { total_cs } => {
52                        let frame_count = segment_frames.len();
53                        if frame_count > 0 {
54                            let per_frame_delay = *total_cs / frame_count as u16;
55                            let remainder = *total_cs % frame_count as u16;
56
57                            for (i, frame) in segment_frames.iter().enumerate() {
58                                let delay = if (i as u16) < remainder {
59                                    per_frame_delay + 1
60                                } else {
61                                    per_frame_delay
62                                };
63                                segment_output
64                                    .push(EncodableFrame::new(frame.frame.image.clone(), delay));
65                            }
66                        }
67                    }
68                    SegmentOp::Scale { factor } => {
69                        for frame in segment_frames {
70                            let original_delay = frame.frame.delay_centiseconds as f64;
71                            let new_delay = (original_delay * factor).round() as u16;
72                            segment_output.push(EncodableFrame::new(
73                                frame.frame.image.clone(),
74                                new_delay.max(1),
75                            ));
76                        }
77                    }
78                    SegmentOp::SetFrameDelay { delay_cs } => {
79                        for frame in segment_frames {
80                            segment_output
81                                .push(EncodableFrame::new(frame.frame.image.clone(), *delay_cs));
82                        }
83                    }
84                }
85                segment_output
86            })
87            .flatten()
88            .collect()
89    }
90
91    #[cfg(not(feature = "parallel"))]
92    {
93        let mut output = Vec::new();
94
95        for segment in segments {
96            let op = ops.get(&segment.id).unwrap_or(&SegmentOp::Keep);
97            let segment_frames = &frames[segment.frame_range.clone()];
98
99            match op {
100                SegmentOp::Keep => {
101                    for frame in segment_frames {
102                        output.push(EncodableFrame::from_decoded(&frame.frame));
103                    }
104                }
105
106                SegmentOp::Remove => {}
107
108                SegmentOp::Collapse { delay_cs } => {
109                    if let Some(first) = segment_frames.first() {
110                        output.push(EncodableFrame::new(first.frame.image.clone(), *delay_cs));
111                    }
112                }
113
114                SegmentOp::SetDuration { total_cs } => {
115                    // Distribute duration evenly across all frames
116                    let frame_count = segment_frames.len();
117                    if frame_count > 0 {
118                        let per_frame_delay = *total_cs / frame_count as u16;
119                        let remainder = *total_cs % frame_count as u16;
120
121                        for (i, frame) in segment_frames.iter().enumerate() {
122                            // Add 1 to first `remainder` frames to distribute evenly
123                            let delay = if (i as u16) < remainder {
124                                per_frame_delay + 1
125                            } else {
126                                per_frame_delay
127                            };
128                            output.push(EncodableFrame::new(frame.frame.image.clone(), delay));
129                        }
130                    }
131                }
132
133                SegmentOp::Scale { factor } => {
134                    // Scale each frame's delay by the factor
135                    for frame in segment_frames {
136                        let original_delay = frame.frame.delay_centiseconds as f64;
137                        let new_delay = (original_delay * factor).round() as u16;
138                        // Ensure minimum delay of 1 centisecond
139                        let new_delay = new_delay.max(1);
140                        output.push(EncodableFrame::new(frame.frame.image.clone(), new_delay));
141                    }
142                }
143
144                SegmentOp::SetFrameDelay { delay_cs } => {
145                    // Set the same delay for all frames
146                    for frame in segment_frames {
147                        output.push(EncodableFrame::new(frame.frame.image.clone(), *delay_cs));
148                    }
149                }
150            }
151        }
152
153        output
154    }
155}
156
157/// Calculate the impact of segment operations without cloning images.
158///
159/// Returns (total_frames, total_duration_cs).
160pub fn dry_run_segment_operations<H: Sync + Send>(
161    frames: &[AnalyzedFrame<H>],
162    segments: &[Segment],
163    ops: &SegmentOps,
164) -> (usize, u64) {
165    let mut total_frames = 0;
166    let mut total_duration_cs: u64 = 0;
167
168    for segment in segments {
169        let op = ops.get(&segment.id).unwrap_or(&SegmentOp::Keep);
170        let segment_frames = &frames[segment.frame_range.clone()];
171
172        match op {
173            SegmentOp::Keep => {
174                total_frames += segment_frames.len();
175                total_duration_cs += segment.total_duration_cs as u64;
176            }
177            SegmentOp::Remove => {}
178            SegmentOp::Collapse { delay_cs } => {
179                if !segment_frames.is_empty() {
180                    total_frames += 1;
181                    total_duration_cs += *delay_cs as u64;
182                }
183            }
184            SegmentOp::SetDuration { total_cs } => {
185                if !segment_frames.is_empty() {
186                    total_frames += segment_frames.len();
187                    total_duration_cs += *total_cs as u64;
188                }
189            }
190            SegmentOp::Scale { factor } => {
191                for frame in segment_frames {
192                    let original_delay = frame.frame.delay_centiseconds as f64;
193                    let new_delay = (original_delay * factor).round() as u16;
194                    total_frames += 1;
195                    total_duration_cs += new_delay.max(1) as u64;
196                }
197            }
198            SegmentOp::SetFrameDelay { delay_cs } => {
199                for _ in segment_frames {
200                    total_frames += 1;
201                    total_duration_cs += *delay_cs as u64;
202                }
203            }
204        }
205    }
206
207    (total_frames, total_duration_cs)
208}
209///
210/// This is the enhanced version that handles frame-level operations like
211/// individual frame removal and segment splitting.
212///
213/// # Arguments
214///
215/// * `frames` - The analyzed frames
216/// * `segments` - The detected segments
217/// * `segment_ops` - Operations to apply to each segment (by segment ID)
218/// * `frame_ops` - Operations to apply to individual frames (by segment ID + frame index)
219///
220/// # Returns
221///
222/// A vector of encodable frames with all operations applied.
223pub fn apply_operations<H: Sync + Send>(
224    frames: &[AnalyzedFrame<H>],
225    segments: &[Segment],
226    segment_ops: &SegmentOps,
227    frame_ops: &FrameOps,
228) -> Vec<EncodableFrame> {
229    #[cfg(feature = "parallel")]
230    {
231        segments
232            .par_iter()
233            .map(|segment| {
234                let seg_op = segment_ops.get(&segment.id).unwrap_or(&SegmentOp::Keep);
235                let segment_frames = &frames[segment.frame_range.clone()];
236                let mut segment_output = Vec::new();
237
238                // If segment-level operation is Remove, skip the entire segment
239                if matches!(seg_op, SegmentOp::Remove) {
240                    return segment_output;
241                }
242
243                // If segment-level operation is not Keep, apply it without frame ops
244                if !matches!(seg_op, SegmentOp::Keep) {
245                    match seg_op {
246                        SegmentOp::Collapse { delay_cs } => {
247                            if let Some(first) = segment_frames.first() {
248                                segment_output.push(EncodableFrame::new(
249                                    first.frame.image.clone(),
250                                    *delay_cs,
251                                ));
252                            }
253                        }
254                        SegmentOp::SetDuration { total_cs } => {
255                            let frame_count = segment_frames.len();
256                            if frame_count > 0 {
257                                let per_frame_delay = *total_cs / frame_count as u16;
258                                let remainder = *total_cs % frame_count as u16;
259                                for (i, frame) in segment_frames.iter().enumerate() {
260                                    let delay = if (i as u16) < remainder {
261                                        per_frame_delay + 1
262                                    } else {
263                                        per_frame_delay
264                                    };
265                                    segment_output.push(EncodableFrame::new(
266                                        frame.frame.image.clone(),
267                                        delay,
268                                    ));
269                                }
270                            }
271                        }
272                        SegmentOp::Scale { factor } => {
273                            for frame in segment_frames {
274                                let original_delay = frame.frame.delay_centiseconds as f64;
275                                let new_delay = (original_delay * factor).round() as u16;
276                                let new_delay = new_delay.max(1);
277                                segment_output.push(EncodableFrame::new(
278                                    frame.frame.image.clone(),
279                                    new_delay,
280                                ));
281                            }
282                        }
283                        SegmentOp::SetFrameDelay { delay_cs } => {
284                            for frame in segment_frames {
285                                segment_output.push(EncodableFrame::new(
286                                    frame.frame.image.clone(),
287                                    *delay_cs,
288                                ));
289                            }
290                        }
291                        _ => {}
292                    }
293                } else {
294                    // Segment operation is Keep - apply frame-level operations
295                    for (i, frame) in segment_frames.iter().enumerate() {
296                        let frame_op = frame_ops.get(&(segment.id, i)).unwrap_or(&FrameOp::Keep);
297
298                        match frame_op {
299                            FrameOp::Keep | FrameOp::SplitAfter => {
300                                segment_output.push(EncodableFrame::from_decoded(&frame.frame));
301                            }
302                            FrameOp::Remove => {}
303                        }
304                    }
305                }
306                segment_output
307            })
308            .flatten()
309            .collect()
310    }
311
312    #[cfg(not(feature = "parallel"))]
313    {
314        let mut output = Vec::new();
315
316        for segment in segments {
317            let seg_op = segment_ops.get(&segment.id).unwrap_or(&SegmentOp::Keep);
318            let segment_frames = &frames[segment.frame_range.clone()];
319
320            // If segment-level operation is Remove, skip the entire segment
321            if matches!(seg_op, SegmentOp::Remove) {
322                continue;
323            }
324
325            // If segment-level operation is not Keep, apply it without frame ops
326            if !matches!(seg_op, SegmentOp::Keep) {
327                match seg_op {
328                    SegmentOp::Collapse { delay_cs } => {
329                        if let Some(first) = segment_frames.first() {
330                            output.push(EncodableFrame::new(first.frame.image.clone(), *delay_cs));
331                        }
332                    }
333                    SegmentOp::SetDuration { total_cs } => {
334                        let frame_count = segment_frames.len();
335                        if frame_count > 0 {
336                            let per_frame_delay = *total_cs / frame_count as u16;
337                            let remainder = *total_cs % frame_count as u16;
338                            for (i, frame) in segment_frames.iter().enumerate() {
339                                let delay = if (i as u16) < remainder {
340                                    per_frame_delay + 1
341                                } else {
342                                    per_frame_delay
343                                };
344                                output.push(EncodableFrame::new(frame.frame.image.clone(), delay));
345                            }
346                        }
347                    }
348                    SegmentOp::Scale { factor } => {
349                        for frame in segment_frames {
350                            let original_delay = frame.frame.delay_centiseconds as f64;
351                            let new_delay = (original_delay * factor).round() as u16;
352                            let new_delay = new_delay.max(1);
353                            output.push(EncodableFrame::new(frame.frame.image.clone(), new_delay));
354                        }
355                    }
356                    SegmentOp::SetFrameDelay { delay_cs } => {
357                        for frame in segment_frames {
358                            output.push(EncodableFrame::new(frame.frame.image.clone(), *delay_cs));
359                        }
360                    }
361                    _ => {}
362                }
363                continue;
364            }
365
366            // Segment operation is Keep - apply frame-level operations
367            for (i, frame) in segment_frames.iter().enumerate() {
368                let frame_op = frame_ops.get(&(segment.id, i)).unwrap_or(&FrameOp::Keep);
369
370                match frame_op {
371                    FrameOp::Keep | FrameOp::SplitAfter => {
372                        // Keep the frame (SplitAfter is just a marker for UI, doesn't affect output)
373                        output.push(EncodableFrame::from_decoded(&frame.frame));
374                    }
375                    FrameOp::Remove => {
376                        // Skip this frame
377                    }
378                }
379            }
380        }
381
382        output
383    }
384}
385
386/// Calculate the impact of all operations without cloning images.
387///
388/// Returns (total_frames, total_duration_cs).
389pub fn dry_run_all_operations<H: Sync + Send>(
390    frames: &[AnalyzedFrame<H>],
391    segments: &[Segment],
392    segment_ops: &SegmentOps,
393    frame_ops: &FrameOps,
394) -> (usize, u64) {
395    let mut total_frames = 0;
396    let mut total_duration_cs: u64 = 0;
397
398    for segment in segments {
399        let seg_op = segment_ops.get(&segment.id).unwrap_or(&SegmentOp::Keep);
400        let segment_frames = &frames[segment.frame_range.clone()];
401
402        // If segment-level operation is Remove, skip the entire segment
403        if matches!(seg_op, SegmentOp::Remove) {
404            continue;
405        }
406
407        // If segment-level operation is not Keep, apply it without frame ops
408        if !matches!(seg_op, SegmentOp::Keep) {
409            match seg_op {
410                SegmentOp::Collapse { delay_cs } => {
411                    if !segment_frames.is_empty() {
412                        total_frames += 1;
413                        total_duration_cs += *delay_cs as u64;
414                    }
415                }
416                SegmentOp::SetDuration { total_cs } => {
417                    let frame_count = segment_frames.len();
418                    if frame_count > 0 {
419                        total_frames += frame_count;
420                        total_duration_cs += *total_cs as u64;
421                    }
422                }
423                SegmentOp::Scale { factor } => {
424                    for frame in segment_frames {
425                        let original_delay = frame.frame.delay_centiseconds as f64;
426                        let new_delay = (original_delay * factor).round() as u16;
427                        total_frames += 1;
428                        total_duration_cs += new_delay.max(1) as u64;
429                    }
430                }
431                SegmentOp::SetFrameDelay { delay_cs } => {
432                    for _ in segment_frames {
433                        total_frames += 1;
434                        total_duration_cs += *delay_cs as u64;
435                    }
436                }
437                _ => {}
438            }
439            continue;
440        }
441
442        // Segment operation is Keep - apply frame-level operations
443        for (i, frame) in segment_frames.iter().enumerate() {
444            let frame_op = frame_ops.get(&(segment.id, i)).unwrap_or(&FrameOp::Keep);
445
446            match frame_op {
447                FrameOp::Keep | FrameOp::SplitAfter => {
448                    total_frames += 1;
449                    total_duration_cs += frame.frame.delay_centiseconds as u64;
450                }
451                FrameOp::Remove => {}
452            }
453        }
454    }
455
456    (total_frames, total_duration_cs)
457}
458
459/// Partition existing segments into new ones based on `FrameOp::SplitAfter` markers.
460///
461/// Returns (new_analyzed_frames, new_segments).
462pub fn split_segments_at_points<H: Clone>(
463    frames: &[AnalyzedFrame<H>],
464    segments: &[Segment],
465    frame_ops: &FrameOps,
466) -> (Vec<AnalyzedFrame<H>>, Vec<Segment>) {
467    let mut new_frames = frames.to_vec();
468    let mut new_segments = Vec::new();
469    let mut current_segment_id = 0;
470
471    for segment in segments {
472        let mut sub_segment_start = segment.frame_range.start;
473
474        for i in 0..segment.frame_count() {
475            let abs_idx = segment.frame_range.start + i;
476            let op = frame_ops.get(&(segment.id, i)).unwrap_or(&FrameOp::Keep);
477            let is_last_frame = i == segment.frame_count() - 1;
478
479            if matches!(op, FrameOp::SplitAfter) || is_last_frame {
480                // Split point or end of segment reached
481                let sub_segment_end = abs_idx + 1;
482
483                // Only create if we haven't already processed this sub-segment
484                if sub_segment_start < sub_segment_end {
485                    let sub_range = sub_segment_start..sub_segment_end;
486
487                    // Re-calculate statistics for the new sub-segment
488                    let sub_frames = &new_frames[sub_range.clone()];
489                    let total_duration_cs: u16 = sub_frames.iter().map(|f| f.delay_cs()).sum();
490
491                    // Inherit static status from parent segment
492                    let is_static = segment.is_static;
493
494                    // Create the new segment
495                    new_segments.push(Segment {
496                        id: current_segment_id,
497                        frame_range: sub_range.clone(),
498                        total_duration_cs,
499                        avg_distance: segment.avg_distance,
500                        is_static,
501                    });
502
503                    // Update segment ID for these frames
504                    for f in &mut new_frames[sub_range] {
505                        f.segment_id = Some(current_segment_id);
506                    }
507
508                    current_segment_id += 1;
509                    sub_segment_start = sub_segment_end;
510                }
511            }
512        }
513    }
514
515    (new_frames, new_segments)
516}
517
518/// Compute statistics for a set of segments.
519#[derive(Debug, Clone, Default)]
520pub struct SegmentStats {
521    /// Total number of segments.
522    pub total_segments: usize,
523    /// Number of static segments (all identical frames).
524    pub static_segments: usize,
525    /// Total number of frames.
526    pub total_frames: usize,
527    /// Total duration in centiseconds.
528    pub total_duration_cs: u64,
529    /// Average frames per segment.
530    pub avg_frames_per_segment: f64,
531    /// Average segment duration in centiseconds.
532    pub avg_duration_cs: f64,
533}
534
535impl SegmentStats {
536    /// Compute statistics from a list of segments.
537    pub fn from_segments(segments: &[Segment]) -> Self {
538        if segments.is_empty() {
539            return Self::default();
540        }
541
542        let total_segments = segments.len();
543        let static_segments = segments.iter().filter(|s| s.is_static).count();
544        let total_frames: usize = segments.iter().map(|s| s.frame_count()).sum();
545        let total_duration_cs: u64 = segments.iter().map(|s| s.total_duration_cs as u64).sum();
546
547        Self {
548            total_segments,
549            static_segments,
550            total_frames,
551            total_duration_cs,
552            avg_frames_per_segment: total_frames as f64 / total_segments as f64,
553            avg_duration_cs: total_duration_cs as f64 / total_segments as f64,
554        }
555    }
556
557    /// Total duration in milliseconds.
558    pub fn total_duration_ms(&self) -> u64 {
559        self.total_duration_cs * 10
560    }
561}
562
563/// Find segments that are likely "pause" segments (static with long duration).
564///
565/// These are good candidates for collapsing or removing.
566pub fn find_pause_segments(segments: &[Segment], min_duration_cs: u16) -> Vec<usize> {
567    segments
568        .iter()
569        .filter(|s| s.is_static && s.total_duration_cs >= min_duration_cs)
570        .map(|s| s.id)
571        .collect()
572}
573
574/// Find the longest segment.
575pub fn find_longest_segment(segments: &[Segment]) -> Option<&Segment> {
576    segments.iter().max_by_key(|s| s.total_duration_cs)
577}
578
579/// Suggest operations to reduce GIF duration by targeting static segments.
580///
581/// This creates operations that collapse long static segments to a shorter duration.
582///
583/// # Arguments
584///
585/// * `segments` - The detected segments
586/// * `target_reduction` - Target reduction ratio (0.5 = reduce duration by half)
587/// * `max_static_duration_cs` - Maximum duration for static segments after reduction
588pub fn suggest_compression_ops(
589    segments: &[Segment],
590    target_reduction: f64,
591    max_static_duration_cs: u16,
592) -> SegmentOps {
593    let mut ops = SegmentOps::new();
594
595    for segment in segments {
596        if segment.is_static && segment.total_duration_cs > max_static_duration_cs {
597            // Collapse long static segments
598            let new_duration = ((segment.total_duration_cs as f64 * target_reduction) as u16)
599                .max(max_static_duration_cs.min(segment.total_duration_cs));
600            ops.insert(
601                segment.id,
602                SegmentOp::Collapse {
603                    delay_cs: new_duration,
604                },
605            );
606        }
607    }
608
609    ops
610}
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    #[test]
617    fn test_segment_stats_empty() {
618        let stats = SegmentStats::from_segments(&[]);
619        assert_eq!(stats.total_segments, 0);
620        assert_eq!(stats.total_frames, 0);
621    }
622
623    #[test]
624    fn test_find_pause_segments() {
625        let segments = vec![
626            Segment {
627                id: 0,
628                frame_range: 0..5,
629                total_duration_cs: 50,
630                avg_distance: 0.0,
631                is_static: true,
632            },
633            Segment {
634                id: 1,
635                frame_range: 5..10,
636                total_duration_cs: 100,
637                avg_distance: 2.0,
638                is_static: false,
639            },
640            Segment {
641                id: 2,
642                frame_range: 10..15,
643                total_duration_cs: 200,
644                avg_distance: 0.0,
645                is_static: true,
646            },
647        ];
648
649        let pauses = find_pause_segments(&segments, 100);
650        assert_eq!(pauses, vec![2]);
651    }
652
653    #[test]
654    fn test_split_segments_at_points() {
655        use crate::types::DecodedFrame;
656        use image::RgbaImage;
657
658        let frames: Vec<AnalyzedFrame<()>> = (0..10)
659            .map(|i| {
660                AnalyzedFrame::new(
661                    DecodedFrame {
662                        index: i,
663                        image: RgbaImage::new(1, 1),
664                        delay_centiseconds: 10,
665                        disposal: crate::types::DisposalMethod::Keep,
666                        left: 0,
667                        top: 0,
668                    },
669                    (),
670                )
671            })
672            .collect();
673
674        let segments = vec![Segment {
675            id: 0,
676            frame_range: 0..10,
677            total_duration_cs: 100,
678            avg_distance: 0.0,
679            is_static: true,
680        }];
681
682        let mut frame_ops = FrameOps::new();
683        // Split after frame 2 (making a 3-frame segment and a 7-frame segment)
684        frame_ops.insert((0, 2), FrameOp::SplitAfter);
685
686        let (new_frames, new_segments) = split_segments_at_points(&frames, &segments, &frame_ops);
687
688        assert_eq!(new_segments.len(), 2);
689        assert_eq!(new_segments[0].id, 0);
690        assert_eq!(new_segments[0].frame_range, 0..3);
691        assert_eq!(new_segments[0].total_duration_cs, 30);
692
693        assert_eq!(new_segments[1].id, 1);
694        assert_eq!(new_segments[1].frame_range, 3..10);
695        assert_eq!(new_segments[1].total_duration_cs, 70);
696
697        // Verify frame assignments
698        assert_eq!(new_frames[0].segment_id, Some(0));
699        assert_eq!(new_frames[2].segment_id, Some(0));
700        assert_eq!(new_frames[3].segment_id, Some(1));
701        assert_eq!(new_frames[9].segment_id, Some(1));
702    }
703}