fix(gif): stop emitting GIFs Discord can't decode; stop dropping 44% of frames (#2)
Fixes mort#1510. Verified end-to-end: the rebuilt file plays correctly in Discord where the original flashes, and is smaller (1210 KB -> 918 KB).
This commit was merged in pull request #2.
This commit is contained in:
+26
-3
@@ -367,13 +367,36 @@ class Animation:
|
|||||||
_run(["gifski", "-o", out, "--fps", str(self.fps), "--quality", "90",
|
_run(["gifski", "-o", out, "--fps", str(self.fps), "--quality", "90",
|
||||||
"--width", str(min(long_edge, self.width))] + fs)
|
"--width", str(min(long_edge, self.width))] + fs)
|
||||||
else:
|
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"split[s0][s1];[s0]palettegen=stats_mode=diff[p];"
|
||||||
f"[s1][p]paletteuse=dither=bayer:bayer_scale=3")
|
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])
|
"-i", os.path.join(self._scratch, "frame_*.png"), "-vf", vf, out])
|
||||||
if _which("gifsicle"):
|
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):
|
def _mp4(self, out, long_edge, audio=None):
|
||||||
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(self.fps),
|
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(self.fps),
|
||||||
|
|||||||
Reference in New Issue
Block a user