Skip to content

Instantly share code, notes, and snippets.

@Epicpkmn11
Last active April 21, 2022 21:28
Show Gist options
  • Save Epicpkmn11/90d1c3008e61beaf9f928fd57221cd49 to your computer and use it in GitHub Desktop.
Save Epicpkmn11/90d1c3008e61beaf9f928fd57221cd49 to your computer and use it in GitHub Desktop.
gif-input-flag.py - Sets the user input flag on specified frames of a GIF
#!/usr/bin/env python3
"""
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
"""
from argparse import ArgumentParser, FileType
from io import SEEK_CUR
from struct import pack, unpack
def setInputFlag(gif, frames: list[int], output=None) -> None:
if output: # Copy input to the output
output.write(gif.read())
gif.seek(0)
output.seek(0)
# Check that this is a GIF
magic = gif.read(6)
if magic != b"GIF89a" and magic != b"GIF87a":
raise Exception("Input is not a GIF")
__, __, flags, __, __ = unpack("<HHBBB", gif.read(7))
if flags & (1 << 7): # GIF has global color table
gctSize = (2 << (flags & 7)) * 3
gif.seek(gctSize, SEEK_CUR)
# Most of this is just skipping through the file until
# we find what we want, the graphics control extension
frameNo = 0
while (type := gif.read(1)) != b"\x3B": # Loop until the trailer
if type == b"\x21": # Extension
extensionType = gif.read(1)
if extensionType == b"\xF9": # Graphics Control Extension
endOfBlock = unpack("B", gif.read(1))[0] + gif.tell() + 1
# The important part:
if frameNo in frames:
flags, = unpack("B", gif.read(1))
flags |= 1 << 1
gif.seek(-1, SEEK_CUR)
if output:
output.seek(gif.tell())
output.write(pack("B", flags))
else: # Edit in place
gif.write(pack("B", flags))
print(f"Successfully set user input flag on frame #{frameNo}")
gif.seek(endOfBlock)
else:
while size := unpack("B", gif.read(1))[0]:
gif.seek(size, SEEK_CUR)
elif type == b"\x2C": # Image
__, __, __, __, flags = unpack("<HHHHB", gif.read(9))
if flags & (1 << 7): # Frame has local color table
lctSize = (2 << (flags & 7)) * 3
gif.seek(lctSize, SEEK_CUR)
gif.seek(1, SEEK_CUR) # LZW minimum code size
while size := unpack("B", gif.read(1))[0]:
gif.seek(size, SEEK_CUR)
frameNo += 1
else:
raise Exception("Invalid block type")
if __name__ == "__main__":
parser = ArgumentParser(description="Sets the user input flag on specified frames of a GIF")
parser.add_argument("gif", metavar="input.gif", type=FileType("rb+"), help="input GIF file (if no output, edited in place)")
parser.add_argument("frames", type=int, nargs="*", help="Frame(s) to set the user input flag on, starting at 0")
parser.add_argument("-o", "--output", metavar="output.gif", type=FileType("wb"), help="output GIF file")
args = parser.parse_args()
setInputFlag(args.gif, args.frames, args.output)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment