diff --git a/gif/scripts/gifsmith.py b/gif/scripts/gifsmith.py index a7ecc10..b64fd0a 100644 --- a/gif/scripts/gifsmith.py +++ b/gif/scripts/gifsmith.py @@ -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),