use crate::analysis::{AnalysisConfig, analyze_frames};
use crate::decoders::BufferedDecoder;
use crate::error::Result;
use crate::hashers::DHasher;
use crate::segment::{apply_operations, apply_segment_operations};
use crate::traits::{FrameHasher, GifDecoder, GifEncoder};
use crate::types::{
AnalyzedFrame, EncodableFrame, EncodeConfig, FrameOps, GifMetadata, Segment, SegmentOp,
SegmentOps,
};
use std::path::Path;
use std::sync::Arc;
#[cfg(feature = "parallel")]
use crate::analysis::analyze_frames_parallel;
pub type ProgressCallback = Arc<dyn Fn(usize, usize) + Send + Sync>;
#[derive(Clone)]
pub struct Figif<H: FrameHasher = DHasher> {
hasher: H,
config: AnalysisConfig,
decoder: BufferedDecoder,
progress_callback: Option<ProgressCallback>,
}
impl<H: FrameHasher + std::fmt::Debug> std::fmt::Debug for Figif<H> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Figif")
.field("hasher", &self.hasher)
.field("config", &self.config)
.field("decoder", &self.decoder)
.field(
"progress_callback",
&self.progress_callback.as_ref().map(|_| "Some(callback)"),
)
.finish()
}
}
impl Default for Figif<DHasher> {
fn default() -> Self {
Self::new()
}
}
impl Figif<DHasher> {
pub fn new() -> Self {
Self {
hasher: DHasher::new(),
config: AnalysisConfig::default(),
decoder: BufferedDecoder::new(),
progress_callback: None,
}
}
}
impl<H: FrameHasher> Figif<H> {
pub fn with_hasher<H2: FrameHasher>(self, hasher: H2) -> Figif<H2> {
Figif {
hasher,
config: self.config,
decoder: self.decoder,
progress_callback: self.progress_callback,
}
}
pub fn with_progress_callback(mut self, callback: ProgressCallback) -> Self {
self.progress_callback = Some(callback);
self
}
pub fn similarity_threshold(mut self, threshold: u32) -> Self {
self.config.similarity_threshold = threshold;
self
}
pub fn min_segment_frames(mut self, min: usize) -> Self {
self.config.min_segment_frames = min.max(1);
self
}
pub fn detect_static(mut self, enabled: bool) -> Self {
self.config.detect_static = enabled;
self
}
pub fn identical_threshold(mut self, threshold: u32) -> Self {
self.config.identical_threshold = threshold;
self
}
pub fn memory_limit(mut self, limit: usize) -> Self {
self.decoder = self.decoder.with_memory_limit(limit);
self
}
pub fn config(&self) -> &AnalysisConfig {
&self.config
}
pub fn hasher(&self) -> &H {
&self.hasher
}
pub fn analyze_file(&self, path: impl AsRef<Path>) -> Result<Analysis<H::Hash>>
where
H::Hash: Send,
{
#[cfg(feature = "parallel")]
{
self.analyze_file_parallel(path)
}
#[cfg(not(feature = "parallel"))]
{
let frames: Vec<_> = self
.decoder
.decode_file(path)?
.collect::<Result<Vec<_>>>()?;
self.analyze_frames(frames)
}
}
pub fn analyze_bytes(&self, data: &[u8]) -> Result<Analysis<H::Hash>>
where
H::Hash: Send,
{
#[cfg(feature = "parallel")]
{
self.analyze_bytes_parallel(data)
}
#[cfg(not(feature = "parallel"))]
{
let frames: Vec<_> = self
.decoder
.decode_bytes(data)?
.collect::<Result<Vec<_>>>()?;
self.analyze_frames(frames)
}
}
pub fn analyze_frames(
&self,
frames: Vec<crate::types::DecodedFrame>,
) -> Result<Analysis<H::Hash>> {
let metadata = if frames.is_empty() {
GifMetadata {
width: 0,
height: 0,
frame_count: 0,
total_duration_ms: 0,
has_transparency: false,
loop_count: crate::types::LoopCount::Infinite,
global_palette: None,
}
} else {
let (width, height) = frames[0].image.dimensions();
let total_duration_ms: u64 = frames.iter().map(|f| f.delay_ms() as u64).sum();
GifMetadata {
width: width as u16,
height: height as u16,
frame_count: frames.len(),
total_duration_ms,
has_transparency: true, loop_count: crate::types::LoopCount::Infinite,
global_palette: None,
}
};
let progress = self
.progress_callback
.as_ref()
.map(|c| c.as_ref() as &(dyn Fn(usize, usize) + Send + Sync));
let (analyzed_frames, segments) =
analyze_frames(frames, &self.hasher, &self.config, progress);
Ok(Analysis {
metadata,
frames: analyzed_frames,
segments,
})
}
#[cfg(feature = "parallel")]
pub fn analyze_file_parallel(&self, path: impl AsRef<Path>) -> Result<Analysis<H::Hash>>
where
H::Hash: Send,
{
let frames: Vec<_> = self
.decoder
.decode_file(path)?
.collect::<Result<Vec<_>>>()?;
self.analyze_frames_parallel(frames)
}
#[cfg(feature = "parallel")]
pub fn analyze_bytes_parallel(&self, data: &[u8]) -> Result<Analysis<H::Hash>>
where
H::Hash: Send,
{
let frames: Vec<_> = self
.decoder
.decode_bytes(data)?
.collect::<Result<Vec<_>>>()?;
self.analyze_frames_parallel(frames)
}
#[cfg(feature = "parallel")]
pub fn analyze_frames_parallel(
&self,
frames: Vec<crate::types::DecodedFrame>,
) -> Result<Analysis<H::Hash>>
where
H::Hash: Send,
{
let metadata = if frames.is_empty() {
GifMetadata {
width: 0,
height: 0,
frame_count: 0,
total_duration_ms: 0,
has_transparency: false,
loop_count: crate::types::LoopCount::Infinite,
global_palette: None,
}
} else {
let (width, height) = frames[0].image.dimensions();
let total_duration_ms: u64 = frames.iter().map(|f| f.delay_ms() as u64).sum();
GifMetadata {
width: width as u16,
height: height as u16,
frame_count: frames.len(),
total_duration_ms,
has_transparency: true,
loop_count: crate::types::LoopCount::Infinite,
global_palette: None,
}
};
let progress = self
.progress_callback
.as_ref()
.map(|c| c.as_ref() as &(dyn Fn(usize, usize) + Send + Sync));
let (analyzed_frames, segments) =
analyze_frames_parallel(frames, &self.hasher, &self.config, progress);
Ok(Analysis {
metadata,
frames: analyzed_frames,
segments,
})
}
}
#[derive(Debug, Clone)]
pub struct Analysis<H> {
pub metadata: GifMetadata,
pub frames: Vec<AnalyzedFrame<H>>,
pub segments: Vec<Segment>,
}
impl<H: Clone + Sync + Send> Analysis<H> {
pub fn frame_count(&self) -> usize {
self.frames.len()
}
pub fn segment_count(&self) -> usize {
self.segments.len()
}
pub fn total_duration_ms(&self) -> u64 {
self.metadata.total_duration_ms
}
pub fn static_segments(&self) -> Vec<&Segment> {
self.segments.iter().filter(|s| s.is_static).collect()
}
pub fn apply_operations(&self, ops: &SegmentOps) -> Vec<EncodableFrame> {
apply_segment_operations(&self.frames, &self.segments, ops)
}
pub fn export<E: GifEncoder>(
&self,
encoder: &E,
ops: &SegmentOps,
config: &EncodeConfig,
) -> Result<Vec<u8>> {
let frames = self.apply_operations(ops);
encoder.encode(&frames, config)
}
pub fn export_to_file<E: GifEncoder>(
&self,
encoder: &E,
ops: &SegmentOps,
path: impl AsRef<Path>,
config: &EncodeConfig,
) -> Result<()> {
let frames = self.apply_operations(ops);
encoder.encode_to_file(&frames, path, config)
}
pub fn apply_all_operations(
&self,
segment_ops: &SegmentOps,
frame_ops: &FrameOps,
) -> Vec<EncodableFrame> {
apply_operations(&self.frames, &self.segments, segment_ops, frame_ops)
}
pub fn calculate_impact(&self, segment_ops: &SegmentOps, frame_ops: &FrameOps) -> (usize, u64) {
let (frames, cs) = crate::segment::dry_run_all_operations(
&self.frames,
&self.segments,
segment_ops,
frame_ops,
);
(frames, cs * 10)
}
pub fn export_with_frame_ops<E: GifEncoder>(
&self,
encoder: &E,
segment_ops: &SegmentOps,
frame_ops: &FrameOps,
config: &EncodeConfig,
) -> Result<Vec<u8>> {
let frames = self.apply_all_operations(segment_ops, frame_ops);
encoder.encode(&frames, config)
}
pub fn export_to_file_with_frame_ops<E: GifEncoder>(
&self,
encoder: &E,
segment_ops: &SegmentOps,
frame_ops: &FrameOps,
path: impl AsRef<Path>,
config: &EncodeConfig,
) -> Result<()> {
let frames = self.apply_all_operations(segment_ops, frame_ops);
encoder.encode_to_file(&frames, path, config)
}
pub fn split_segments(&self, frame_ops: &FrameOps) -> Analysis<H> {
let (new_frames, new_segments) =
crate::segment::split_segments_at_points(&self.frames, &self.segments, frame_ops);
Analysis {
metadata: self.metadata.clone(),
frames: new_frames,
segments: new_segments,
}
}
pub fn as_encodable(&self) -> Vec<EncodableFrame> {
self.frames
.iter()
.map(|f| EncodableFrame::from_decoded(&f.frame))
.collect()
}
pub fn pauses(&self) -> crate::selector::SegmentSelector<'_> {
let segments = self.segments.iter().filter(|s| s.is_static).collect();
crate::selector::SegmentSelector::new(segments)
}
pub fn motion(&self) -> crate::selector::SegmentSelector<'_> {
let segments = self.segments.iter().filter(|s| !s.is_static).collect();
crate::selector::SegmentSelector::new(segments)
}
pub fn all(&self) -> crate::selector::SegmentSelector<'_> {
let segments = self.segments.iter().collect();
crate::selector::SegmentSelector::new(segments)
}
pub fn segment(&self, id: usize) -> crate::selector::SegmentSelector<'_> {
let segments = self.segments.iter().filter(|s| s.id == id).collect();
crate::selector::SegmentSelector::new(segments)
}
pub fn segments_by_id(&self, ids: &[usize]) -> crate::selector::SegmentSelector<'_> {
let segments = self
.segments
.iter()
.filter(|s| ids.contains(&s.id))
.collect();
crate::selector::SegmentSelector::new(segments)
}
pub fn frames_range(
&self,
range: std::ops::Range<usize>,
) -> crate::selector::SegmentSelector<'_> {
let segments = self
.segments
.iter()
.filter(|s| {
s.frame_range.start < range.end && s.frame_range.end > range.start
})
.collect();
crate::selector::SegmentSelector::new(segments)
}
pub fn cap_pauses(&self, max_ms: u32) -> SegmentOps {
let max_cs = (max_ms / 10) as u16;
let mut ops = SegmentOps::new();
for segment in &self.segments {
if segment.is_static && segment.total_duration_cs > max_cs {
ops.insert(segment.id, SegmentOp::Collapse { delay_cs: max_cs });
}
}
ops
}
pub fn collapse_all_pauses(&self, duration_ms: u32) -> SegmentOps {
let delay_cs = (duration_ms / 10) as u16;
let mut ops = SegmentOps::new();
for segment in &self.segments {
if segment.is_static {
ops.insert(segment.id, SegmentOp::Collapse { delay_cs });
}
}
ops
}
pub fn remove_long_pauses(&self, min_ms: u32) -> SegmentOps {
let min_cs = (min_ms / 10) as u16;
let mut ops = SegmentOps::new();
for segment in &self.segments {
if segment.is_static && segment.total_duration_cs >= min_cs {
ops.insert(segment.id, SegmentOp::Remove);
}
}
ops
}
pub fn speed_up_pauses(&self, factor: f64) -> SegmentOps {
let mut ops = SegmentOps::new();
for segment in &self.segments {
if segment.is_static {
ops.insert(
segment.id,
SegmentOp::Scale {
factor: 1.0 / factor,
},
);
}
}
ops
}
pub fn speed_up_all(&self, factor: f64) -> SegmentOps {
let mut ops = SegmentOps::new();
for segment in &self.segments {
ops.insert(
segment.id,
SegmentOp::Scale {
factor: 1.0 / factor,
},
);
}
ops
}
pub fn target_duration(&self, target_ms: u64) -> Option<SegmentOps> {
let current_ms = self.total_duration_ms();
if current_ms <= target_ms {
return Some(SegmentOps::new()); }
let to_remove_ms = current_ms - target_ms;
let static_duration_ms: u64 = self
.segments
.iter()
.filter(|s| s.is_static)
.map(|s| s.duration_ms() as u64)
.sum();
if static_duration_ms < to_remove_ms {
return None;
}
let mut static_segs: Vec<_> = self.segments.iter().filter(|s| s.is_static).collect();
static_segs.sort_by(|a, b| b.total_duration_cs.cmp(&a.total_duration_cs));
let mut ops = SegmentOps::new();
let mut removed_ms: u64 = 0;
let min_pause_ms = 100;
for segment in static_segs {
if removed_ms >= to_remove_ms {
break;
}
let segment_ms = segment.duration_ms() as u64;
let can_remove = segment_ms.saturating_sub(min_pause_ms as u64);
if can_remove > 0 {
let to_remove_from_this = can_remove.min(to_remove_ms - removed_ms);
let new_duration_ms = segment_ms - to_remove_from_this;
if new_duration_ms <= min_pause_ms as u64 {
ops.insert(
segment.id,
SegmentOp::Collapse {
delay_cs: (min_pause_ms / 10) as u16,
},
);
} else {
ops.insert(
segment.id,
SegmentOp::Collapse {
delay_cs: (new_duration_ms / 10) as u16,
},
);
}
removed_ms += to_remove_from_this;
}
}
Some(ops)
}
pub fn merge_ops(op_sets: &[SegmentOps]) -> SegmentOps {
let mut merged = SegmentOps::new();
for ops in op_sets {
merged.extend(ops.iter().map(|(k, v)| (*k, v.clone())));
}
merged
}
}