Coverage for pyguymer3/image/gifsicle.py: 4%
23 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def gifsicle(
5 fname1,
6 /,
7 *,
8 chunksize = 1048576,
9 debug = __debug__,
10 gifsiclePath = None,
11 timeout = 60.0,
12):
13 """
14 "gifsicle" does modify, and it does touch, the image even if it cannot make
15 it smaller, therefore it is NOT safe to keep on running "gifsicle" on the
16 same GIF over and over again.
18 In my own testing (August 2020) I have found that "gifsicle" switches
19 between two different images when it is run repeatadly (try finding the MD5
20 hash of the images) and that the only differences between these identically
21 sized images is the order of the colours in the colour map (try running
22 "gifsicle --color-info file.gif" yourself after each call and then "diff"
23 the output).
25 chunksize : int, optional
26 the size of the chunks of any files which are read in (in bytes)
27 """
29 # Import standard modules ...
30 import os
31 import shutil
32 import subprocess
33 import tempfile
35 # Import sub-functions ...
36 from ..sha512 import sha512
38 # **************************************************************************
40 # Try to find the paths if the user did not provide them ...
41 if gifsiclePath is None:
42 gifsiclePath = shutil.which("gifsicle")
43 assert gifsiclePath is not None, "\"gifsicle\" is not installed"
45 # Check that the image exists ...
46 if not os.path.exists(fname1):
47 raise Exception(f"\"{fname1}\" does not exist") from None
49 # Create temporary directory ...
50 with tempfile.TemporaryDirectory(prefix = "gifsicle.") as tname:
51 # Create temporary name ...
52 fname2 = f"{tname}/image.gif"
54 # Optimise GIF ...
55 subprocess.run(
56 [
57 gifsiclePath,
58 "--unoptimize",
59 "--optimize=3",
60 "--output", fname2,
61 fname1,
62 ],
63 check = True,
64 encoding = "utf-8",
65 stderr = subprocess.DEVNULL,
66 stdout = subprocess.DEVNULL,
67 timeout = timeout,
68 )
70 # Find the two sizes and don't replace the original if the new one is
71 # larger, or equal ...
72 if os.path.getsize(fname2) >= os.path.getsize(fname1):
73 if debug:
74 print(f"INFO: Skipping because \"{fname2}\" is larger than, or equal to, \"{fname1}\"")
75 return
77 # Find the two hashes and don't replace the original if the new one is
78 # the same ...
79 if sha512(fname1, chunksize = chunksize) == sha512(fname2, chunksize = chunksize):
80 if debug:
81 print(f"INFO: Skipping because \"{fname2}\" is the same as \"{fname1}\"")
82 return
84 # Replace the original ...
85 shutil.move(fname2, fname1)