fix(gif): stop emitting GIFs Discord can't decode; stop dropping 44% of frames #2

Merged
steve merged 1 commits from fix/gif-discord-local-palettes into main 2026-07-25 21:10:54 +00:00
Showing only changes of commit 613945ff36 - Show all commits
+26 -3
View File
@@ -367,13 +367,36 @@ class Animation:
_run(["gifski", "-o", out, "--fps", str(self.fps), "--quality", "90",
"--width", str(min(long_edge, self.width))] + fs)
else:
vf = (f"fps={self.fps},scale='min({long_edge},iw)':-1:flags=lanczos,"
# -framerate applies to the INPUT. Without it ffmpeg reads a PNG
# sequence at its default 25fps, and a fps= filter then resamples
# 25 -> self.fps: on the #1510 render that silently dropped 91 of
# 207 frames (44%) and played the rest too fast. _mp4 below always
# passed -framerate; this path never did. With the input rate set
# correctly the fps= filter is redundant — and removing it is what
# takes the output from 205 frames back to all 207.
vf = (f"scale='min({long_edge},iw)':-1:flags=lanczos,"
f"split[s0][s1];[s0]palettegen=stats_mode=diff[p];"
f"[s1][p]paletteuse=dither=bayer:bayer_scale=3")
_run(["ffmpeg", "-y", "-loglevel", "error", "-pattern_type", "glob",
_run(["ffmpeg", "-y", "-loglevel", "error",
"-framerate", str(self.fps), "-pattern_type", "glob",
"-i", os.path.join(self._scratch, "frame_*.png"), "-vf", vf, out])
if _which("gifsicle"):
_run(["gifsicle", "-O3", "--lossy=60", out, "-o", out])
# --colors 256 is load-bearing for DISCORD, not for size.
#
# gifski gives every frame its OWN local colour table (that is where
# its quality comes from). Stack gifsicle's cross-frame transparency
# on top and the result is a GIF that gifsicle itself refuses to
# reopen: "too complex to unoptimize — local colour tables or complex
# transparency". PIL, ffmpeg and macOS Preview all render it fine;
# Discord's decoder does not, and paints later scenes with stale rows,
# ghosted captions and holes punched in solid shapes (#1510).
#
# Collapsing to ONE global palette costs ~6/255 mean colour error
# (visible as slight banding in gradients, since one palette now
# spans every scene) and is the price of playing in the client that
# actually matters. It also came out SMALLER on the reported file:
# 1210 KB -> 915 KB.
_run(["gifsicle", "--colors", "256", "-O3", "--lossy=60", out, "-o", out])
def _mp4(self, out, long_edge, audio=None):
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(self.fps),