Save frames as optimized GIF for Slack. Args: output_path: Where to save the GIF num_colors: Number of colors to use (fewer = smaller file) optimize_for_emoji: If True, optimize for <64KB emoji size remove_duplicates: Remove duplicate
(self, output_path: str | Path, num_colors: int = 128,
optimize_for_emoji: bool = False, remove_duplicates: bool = True)
| 146 | return removed_count |
| 147 | |
| 148 | def save(self, output_path: str | Path, num_colors: int = 128, |
| 149 | optimize_for_emoji: bool = False, remove_duplicates: bool = True) -> dict: |
| 150 | """ |
| 151 | Save frames as optimized GIF for Slack. |
| 152 | |
| 153 | Args: |
| 154 | output_path: Where to save the GIF |
| 155 | num_colors: Number of colors to use (fewer = smaller file) |
| 156 | optimize_for_emoji: If True, optimize for <64KB emoji size |
| 157 | remove_duplicates: Remove duplicate consecutive frames |
| 158 | |
| 159 | Returns: |
| 160 | Dictionary with file info (path, size, dimensions, frame_count) |
| 161 | """ |
| 162 | if not self.frames: |
| 163 | raise ValueError("No frames to save. Add frames with add_frame() first.") |
| 164 | |
| 165 | output_path = Path(output_path) |
| 166 | original_frame_count = len(self.frames) |
| 167 | |
| 168 | # Remove duplicate frames to reduce file size |
| 169 | if remove_duplicates: |
| 170 | removed = self.deduplicate_frames(threshold=0.98) |
| 171 | if removed > 0: |
| 172 | print(f" Removed {removed} duplicate frames") |
| 173 | |
| 174 | # Optimize for emoji if requested |
| 175 | if optimize_for_emoji: |
| 176 | if self.width > 128 or self.height > 128: |
| 177 | print(f" Resizing from {self.width}x{self.height} to 128x128 for emoji") |
| 178 | self.width = 128 |
| 179 | self.height = 128 |
| 180 | # Resize all frames |
| 181 | resized_frames = [] |
| 182 | for frame in self.frames: |
| 183 | pil_frame = Image.fromarray(frame) |
| 184 | pil_frame = pil_frame.resize((128, 128), Image.Resampling.LANCZOS) |
| 185 | resized_frames.append(np.array(pil_frame)) |
| 186 | self.frames = resized_frames |
| 187 | num_colors = min(num_colors, 48) # More aggressive color limit for emoji |
| 188 | |
| 189 | # More aggressive FPS reduction for emoji |
| 190 | if len(self.frames) > 12: |
| 191 | print(f" Reducing frames from {len(self.frames)} to ~12 for emoji size") |
| 192 | # Keep every nth frame to get close to 12 frames |
| 193 | keep_every = max(1, len(self.frames) // 12) |
| 194 | self.frames = [self.frames[i] for i in range(0, len(self.frames), keep_every)] |
| 195 | |
| 196 | # Optimize colors with global palette |
| 197 | optimized_frames = self.optimize_colors(num_colors, use_global_palette=True) |
| 198 | |
| 199 | # Calculate frame duration in milliseconds |
| 200 | frame_duration = 1000 / self.fps |
| 201 | |
| 202 | # Save GIF |
| 203 | imageio.imwrite( |
| 204 | output_path, |
| 205 | optimized_frames, |