You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
104 lines
3.5 KiB
104 lines
3.5 KiB
#!/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)
|