Skip to content

Instantly share code, notes, and snippets.

@TheDucker1
Last active July 3, 2024 16:53
Show Gist options
  • Select an option

  • Save TheDucker1/cfb8d9112a5fc1244267143f61060a2f to your computer and use it in GitHub Desktop.

Select an option

Save TheDucker1/cfb8d9112a5fc1244267143f61060a2f to your computer and use it in GitHub Desktop.
call gifski library and ffmpeg with python (windows)
#requires:
# gifski: https://gif.ski/
# ffmpeg: https://ffmpeg.org/
import subprocess
import argparse
import ctypes
import os.path
#replace your path here
GIFSKI_PATH = os.path.join("C:/", "Users", "PC", "Downloads", "tools", "gifski", "developer", "gifski.dll")
FFMPEG_PATH = os.path.join("C:/", "Users", "PC", "Downloads", "tools", "ffmpeg", "bin", "ffmpeg.exe")
# gifski.h
class GifskiSettings(ctypes.Structure):
_fields_ = [
('width', ctypes.c_uint32), #Resize to max this width if non-0.
('height', ctypes.c_uint32), #Resize to max this height if width is non-0. Note that aspect ratio is not preserved.
('quality', ctypes.c_uint8), #1-100, but useful range is 50-100. Recommended to set to 90.
('fast', ctypes.c_bool), #Lower quality, but faster encode.
('repeat', ctypes.c_int16) #If negative, looping is disabled. The number of times the sequence is repeated. 0 to loop forever.
]
class GifskiError():
GIFSKI_OK = 0
GIFSKI_NULL_ARG = 1 #one of input arguments was NULL
GIFSKI_INVALID_STATE = 2 #a one-time function was called twice, or functions were called in wrong order
GIFSKI_QUANT = 3 #internal error related to palette quantization
GIFSKI_GIF = 4 #internal error related to gif composing
GIFSKI_THREAD_LOST = 5 #internal error - unexpectedly aborted
GIFSKI_NOT_FOUND = 6 #I/O error: file or directory not found
GIFSKI_PERMISSION_DENIED = 7 #I/O error: permission denied
GIFSKI_ALREADY_EXISTS = 8 #I/O error: file already exists
GIFSKI_INVALID_INPUT = 9 #invalid arguments passed to function
GIFSKI_TIMED_OUT = 10 #misc I/O error
GIFSKI_WRITE_ZERO = 11 #misc I/O error
GIFSKI_INTERRUPTED = 12 #misc I/O error
GIFSKI_UNEXPECTED_EOF = 13 #misc I/O error
GIFSKI_ABORTED = 14 #progress callback returned 0, writing aborted
GIFSKI_OTHER = 15 #should not happen, file a bug
gifskiLib = ctypes.CDLL(GIFSKI_PATH)
#Call to start the process
#See `gifski_add_frame_png_file` and `gifski_end_adding_frames`
#Returns a handle for the other functions, or `NULL` on error (if the settings are invalid).
# -- gifski *gifski_new(const GifskiSettings *settings);
gifskiLib.gifski_new.argtypes = [ctypes.POINTER(GifskiSettings)]
gifskiLib.gifski_new.restype = ctypes.c_void_p
#Quality 1-100 of temporal denoising. Lower values reduce motion. Defaults to `settings.quality`.
#Only valid immediately after calling `gifski_new`, before any frames are added.
# -- GifskiError gifski_set_motion_quality(gifski *handle, uint8_t quality);
gifskiLib.gifski_set_motion_quality.argtypes = [ctypes.c_void_p, ctypes.c_uint8]
#Quality 1-100 of gifsicle compression. Lower values add noise. Defaults to `settings.quality`.
#Has no effect if the `gifsicle` feature hasn't been enabled.
#Only valid immediately after calling `gifski_new`, before any frames are added.
# -- GifskiError gifski_set_lossy_quality(gifski *handle, uint8_t quality);
gifskiLib.gifski_set_lossy_quality.argtypes = [ctypes.c_void_p, ctypes.c_uint8]
#If `true`, encoding will be significantly slower, but may look a bit better.
#Only valid immediately after calling `gifski_new`, before any frames are added.
# -- GifskiError gifski_set_extra_effort(gifski *handle, bool extra);
gifskiLib.gifski_set_extra_effort.argtypes = [ctypes.c_void_p, ctypes.c_bool]
#Adds a frame to the animation. This function is asynchronous.
#File path must be valid UTF-8.
#`frame_number` orders frames (consecutive numbers starting from 0).
#You can add frames in any order, and they will be sorted by their `frame_number`.
#Presentation timestamp (PTS) is time in seconds, since start of the file, when this frame is to be displayed.
#For a 20fps video it could be `frame_number/20.0`.
#Frames with duplicate or out-of-order PTS will be skipped.
#The first frame should have PTS=0. If the first frame has PTS > 0, it'll be used as a delay after the last frame.
#This function may block and wait until the frame is processed. Make sure to call `gifski_set_write_callback` or `gifski_set_file_output` first to avoid a deadlock.
#Returns 0 (`GIFSKI_OK`) on success, and non-0 `GIFSKI_*` constant on error.
# -- GifskiError gifski_add_frame_png_file(gifski *handle,
# -- uint32_t frame_number,
# -- const char *file_path,
# -- double presentation_timestamp);
gifskiLib.gifski_add_frame_png_file.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_double]
# Adds a frame to the animation. This function is asynchronous.
# `pixels` is an array width×height×4 bytes large.
# The array is copied, so you can free/reuse it immediately after this function returns.
# `frame_number` orders frames (consecutive numbers starting from 0).
# You can add frames in any order, and they will be sorted by their `frame_number`.
# However, out-of-order frames are buffered in RAM, and will cause high memory usage
# if there are gaps in the frame numbers.
# Presentation timestamp (PTS) is time in seconds, since start of the file, when this frame is to be displayed.
# For a 20fps video it could be `frame_number/20.0`. First frame must have PTS=0.
# Frames with duplicate or out-of-order PTS will be skipped.
# The first frame should have PTS=0. If the first frame has PTS > 0, it'll be used as a delay after the last frame.
# Colors are in sRGB, uncorrelated RGBA, with alpha byte last.
# This function may block and wait until the frame is processed. Make sure to call `gifski_set_write_callback` or `gifski_set_file_output` first to avoid a deadlock.
# Returns 0 (`GIFSKI_OK`) on success, and non-0 `GIFSKI_*` constant on error.
# -- GifskiError gifski_add_frame_rgba(gifski *handle,
# -- uint32_t frame_number,
# -- uint32_t width,
# -- uint32_t height,
# -- const unsigned char *pixels,
# -- double presentation_timestamp);
gifskiLib.gifski_add_frame_rgba.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_double]
#Same as `gifski_add_frame_rgba`, but with bytes per row arg
# -- GifskiError gifski_add_frame_rgba_stride(gifski *handle,
# -- uint32_t frame_number,
# -- uint32_t width,
# -- uint32_t height,
# -- uint32_t bytes_per_row,
# -- const unsigned char *pixels,
# -- double presentation_timestamp);
gifskiLib.gifski_add_frame_rgba_stride.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_double]
#Same as `gifski_add_frame_rgba_stride`, except it expects components in ARGB order.
#Bytes per row must be multiple of 4, and greater or equal width×4.
#If the bytes per row value is invalid (e.g. an odd number), frames may look sheared/skewed.
#Colors are in sRGB, uncorrelated ARGB, with alpha byte first.
#`gifski_add_frame_rgba` is preferred over this function.
# -- GifskiError gifski_add_frame_argb(gifski *handle,
# -- uint32_t frame_number,
# -- uint32_t width,
# -- uint32_t bytes_per_row,
# -- uint32_t height,
# -- const unsigned char *pixels,
# -- double presentation_timestamp);
gifskiLib.gifski_add_frame_argb.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_double]
# Same as `gifski_add_frame_rgba_stride`, except it expects RGB components (3 bytes per pixel)
# Bytes per row must be multiple of 3, and greater or equal width×3.
# If the bytes per row value is invalid (not multiple of 3), frames may look sheared/skewed.
# Colors are in sRGB, red byte first.
# `gifski_add_frame_rgba` is preferred over this function.
# -- GifskiError gifski_add_frame_rgb(gifski *handle,
# -- uint32_t frame_number,
# -- uint32_t width,
# -- uint32_t bytes_per_row,
# -- uint32_t height,
# -- const unsigned char *pixels,
# -- double presentation_timestamp);
gifskiLib.gifski_add_frame_rgb.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_double]
# Get a callback for frame processed, and abort processing if desired.
# The callback is called once per input frame,
# even if the encoder decides to skip some frames.
# It gets arbitrary pointer (`user_data`) as an argument. `user_data` can be `NULL`.
# The callback must return `1` to continue processing, or `0` to abort.
# The callback must be thread-safe (it will be called from another thread).
# It must remain valid at all times, until `gifski_finish` completes.
# This function must be called before `gifski_set_file_output()` to take effect.
# -- void gifski_set_progress_callback(gifski *handle, int (*progress_callback)(void *user_data), void *user_data);
gifskiLib.gifski_set_progress_callback.argtypes = [ctypes.c_void_p, ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p), ctypes.c_void_p]
# Get a callback when an error occurs.
# This is intended mostly for logging and debugging, not for user interface.
# The callback function has the following arguments:
# * A `\0`-terminated C string in UTF-8 encoding. The string is only valid for the duration of the call. Make a copy if you need to keep it.
# * An arbitrary pointer (`user_data`). `user_data` can be `NULL`.
# The callback must be thread-safe (it will be called from another thread).
# It must remain valid at all times, until `gifski_finish` completes.
# If the callback is not set, errors will be printed to stderr.
# This function must be called before `gifski_set_file_output()` to take effect.
# -- GifskiError gifski_set_error_message_callback(gifski *handle, void (*error_message_callback)(const char*, void*), void *user_data);
gifskiLib.gifski_set_error_message_callback.argtypes = [ctypes.c_void_p, ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p), ctypes.c_void_p]
# Start writing to the file at `destination_path` (overwrites if needed).
# The file path must be ASCII or valid UTF-8.
# This function has to be called before any frames are added.
# This call will not block.
# Returns 0 (`GIFSKI_OK`) on success, and non-0 `GIFSKI_*` constant on error.
# -- GifskiError gifski_set_file_output(gifski *handle, const char *destination_path);
gifskiLib.gifski_set_file_output.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
# Start writing via callback (any buffer, file, whatever you want). This has to be called before any frames are added.
# This call will not block.
# The callback function receives 3 arguments:
# - size of the buffer to write, in bytes. IT MAY BE ZERO (when it's zero, either do nothing, or flush internal buffers if necessary).
# - pointer to the buffer.
# - context pointer to arbitrary user data, same as passed in to this function.
# The callback should return 0 (`GIFSKI_OK`) on success, and non-zero on error.
# The callback function must be thread-safe. It must remain valid at all times, until `gifski_finish` completes.
# Returns 0 (`GIFSKI_OK`) on success, and non-0 `GIFSKI_*` constant on error.
# -- GifskiError gifski_set_write_callback(gifski *handle,
# -- int (*write_callback)(size_t buffer_length, const uint8_t *buffer, void *user_data),
# -- void *user_data);
gifskiLib.gifski_set_write_callback.argtypes = [ctypes.c_void_p, ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint8), ctypes.c_void_p), ctypes.c_void_p]
# The last step:
# - stops accepting any more frames (gifski_add_frame_* calls are blocked)
# - blocks and waits until all already-added frames have finished writing
# Returns final status of write operations. Remember to check the return value!
# Must always be called, otherwise it will leak memory.
# After this call, the handle is freed and can't be used any more.
# Returns 0 (`GIFSKI_OK`) on success, and non-0 `GIFSKI_*` constant on error.
# -- GifskiError gifski_finish(gifski *g);
gifskiLib.gifski_finish.argtypes = [ctypes.c_void_p]
def main(args):
settings = GifskiSettings()
settings.quality = args.quality
cmplx = False
if args.width:
settings.width = args.width
if args.height:
settings.height = args.height
if args.complex:
cmplx = True
output_path = "out.gif".encode("utf-8")
if args.output_name:
output_path = args.output_name.encode("utf-8")
s = gifskiLib.gifski_new(ctypes.cast(ctypes.addressof(settings), ctypes.POINTER(GifskiSettings)))
if cmplx:
inps_ = args.input_name.split()
inps = []
in_quote = False
# group all quotations into a string, then remove the quotes
for _ in inps_:
if not in_quote:
inps += _,
if _[0] == '"' and (len(_) == 1 or _[-1] != '"'):
in_quote = True
else:
inps[-1] += " " + _
if _[-1] == '"':
in_quote = False
inps[-1] = inps[-1][1:-1]
ffmpeg_cmd = [FFMPEG_PATH, *inps]
else:
ffmpeg_cmd = [FFMPEG_PATH, '-i', args.input_name]
if args.width and args.height:
ffmpeg_cmd += ['-vf', f'scale=min({args.width*2}\\,iw):min({args.height*2}\\,ih):lanczos']
elif args.width:
ffmpeg_cmd += ['-vf', f'scale=min({args.width*2}\\,iw):-2:lanczos']
elif args.height:
ffmpeg_cmd += ['-vf', f'scale=-2:min({args.height*2}\\,ih):lanczos']
ffmpeg_cmd += ['-f', 'image2pipe', '-c:v', 'ppm', '-']
print(ffmpeg_cmd)
p = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
try:
assert( gifskiLib.gifski_set_file_output(s, output_path) == GifskiError.GIFSKI_OK )
data = p.stdout.read()
ii = 0
frame=0
while ii < len(data):
#P6\nW H\nMAX\n
nl1, nl2, nl3 = -1, -1, -1
oo = 0
while nl3 == -1:
if data[ii+oo] == 0xA:
if nl1 == -1:
nl1 = ii+oo
elif nl2 == -1:
nl2 = ii+oo
else:
nl3 = ii+oo
oo += 1
W, H = map(int, data[nl1+1:nl2].decode('utf-8').split())
bitmap = ctypes.c_char * (W*H*3)
#bitmap = b''.join(data[nl3+1+i*3:nl3+1+i*3+3] + b'\xFF' for i in range(W*H))
#assert( gifskiLib.gifski_add_frame_rgba(s, frame, W, H, ctypes.c_char_p(bitmap), ctypes.c_double(frame / args.fps)) == GifskiError.GIFSKI_OK )
assert( gifskiLib.gifski_add_frame_rgb(s , frame, W, W*3, H, bitmap.from_buffer_copy(data, nl3+1), ctypes.c_double(frame / args.fps)) == GifskiError.GIFSKI_OK )
frame += 1
ii = nl3+1+(W*H*3)
finally:
p.terminate()
OK = gifskiLib.gifski_finish(s)
del data
assert ( OK == GifskiError.GIFSKI_OK )
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='vid2gif utility',
description='shortcut to use ffmpeg and gifski to create a gif, windows only'
)
parser.add_argument('input_name', help='path to input file', type=str)
parser.add_argument('-o', '--output_name', help='path to output file / directory', type=str)
parser.add_argument('-W', '--width', help='gif width', type=int)
parser.add_argument('-H', '--height', help='gif height', type=int)
parser.add_argument('--quality', help='gifski quality', type=int, default=90)
parser.add_argument('--fps', help='playback fps', type=int, default=24)
parser.add_argument('--complex', help='complex input', action='store_true')
args = parser.parse_args()
main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment