summaryrefslogtreecommitdiff
path: root/src/cursors/anicursorgen.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/cursors/anicursorgen.py')
-rwxr-xr-xsrc/cursors/anicursorgen.py804
1 files changed, 466 insertions, 338 deletions
diff --git a/src/cursors/anicursorgen.py b/src/cursors/anicursorgen.py
index ad4b7c07e..906804806 100755
--- a/src/cursors/anicursorgen.py
+++ b/src/cursors/anicursorgen.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
# -*- coding: utf-8 -*-
# anicursorgen
# Copyright (C) 2015 Руслан Ижбулатов <lrn1986@gmail.com>
@@ -25,368 +25,496 @@ import shlex
import io
import struct
import math
+import functools
from PIL import Image
from PIL import ImageFilter
p = struct.pack
-program_name = 'anicursorgen'
+program_name = "anicursorgen"
program_version = 1.0
-def main ():
- parser = argparse.ArgumentParser (description='Creates .ani or .cur files from separate images and input metadata.',
- add_help=False)
- parser.add_argument ('-V', '--version', action='version', version='{}-{}'.format (program_name, program_version),
- help='Display the version number and exit.')
- parser.add_argument ('-h', '-?', action='help',
- help='Display the usage message and exit.')
- parser.add_argument ('-p', '--prefix', metavar='dir', default=None,
- help='Find cursor images in the directory specified by dir. If not specified, the current directory is used.')
- parser.add_argument ('-s', '--add-shadows', action='store_true', dest='add_shadows', default=False,
- help='Generate shadows for cursors (disabled by default).')
- parser.add_argument ('-n', '--no-shadows', action='store_false', dest='add_shadows', default=False,
- help='Do not generate shadows for cursors (put after --add-shadows to cancel its effect).')
-
- shadows = parser.add_argument_group (title='Shadow generation', description='Only relevant when --add-shadows is given')
-
- shadows.add_argument ('-r', '--right-shift', metavar='%', type=float, default=9.375,
- help='Shift shadow right by this percentage of the canvas size (default is 9.375).')
- shadows.add_argument ('-d', '--down-shift', metavar='%', type=float, default=3.125,
- help='Shift shadow down by this percentage of the canvas size (default is 3.125).')
- shadows.add_argument ('-b', '--blur', metavar='%', type=float, default=3.125,
- help='Blur radius, in percentage of the canvas size (default is 3.125, set to 0 to disable blurring).')
- shadows.add_argument ('-c', '--color', metavar='%', default='0x00000040',
- help='Shadow color in 0xRRGGBBAA form (default is 0x00000040).')
-
- parser.add_argument ('input_config', default='-', metavar='input-config [output-file]', nargs='?',
- help='Input config file (stdin by default).')
- parser.add_argument ('output_file', default='-', metavar='', nargs='?',
- help='Output cursor file (stdout by default).')
-
- args = parser.parse_args ()
-
- try:
- if args.color[0] != '0' or args.color[1] not in ['x', 'X'] or len (args.color) != 10:
- raise ValueError
- args.color = (int (args.color[2:4], 16), int (args.color[4:6], 16), int (args.color[6:8], 16), int (args.color[8:10], 16))
- except:
- print ("Can't parse the color '{}'".format (args.color), file=sys.stderr)
- parser.print_help ()
- return 1
-
- if args.prefix is None:
- args.prefix = os.getcwd ()
-
- if args.input_config == '-':
- input_config = sys.stdin
- else:
- input_config = open (args.input_config, 'rb')
-
- if args.output_file == '-':
- output_file = sys.stdout
- else:
- output_file = open (args.output_file, 'wb')
-
- result = make_cursor_from (input_config, output_file, args)
-
- input_config.close ()
- output_file.close ()
-
- return result
-
-def make_cursor_from (inp, out, args):
- frames = parse_config_from (inp, args.prefix)
-
- animated = frames_have_animation (frames)
-
- if animated:
- result = make_ani (frames, out, args)
- else:
- buf = make_cur (frames, args)
- copy_to (out, buf)
- result = 0
-
- return result
-
-def copy_to (out, buf):
- buf.seek (0, io.SEEK_SET)
- while True:
- b = buf.read (1024)
- if len (b) == 0:
- break
- out.write (b)
-
-def frames_have_animation (frames):
- sizes = set ()
-
- for frame in frames:
- if frame[4] == 0:
- continue
- if frame[0] in sizes:
- return True
- sizes.add (frame[0])
-
- return False
-
-def make_cur (frames, args, animated=False):
- buf = io.BytesIO ()
- buf.write (p ('<HHH', 0, 2, len (frames)))
- frame_offsets = []
-
- def frame_size_cmp (f1, f2):
- if f1[0] < f2[0]:
- return -1
- elif f1[0] > f2[0]:
- return 1
- else:
- return 0
-
- frames = sorted (frames, frame_size_cmp, reverse=True)
-
- for frame in frames:
- width = frame[0]
- if width > 255:
- width = 0
- height = width
- buf.write (p ('<BBBB HH', width, height, 0, 0, frame[1], frame[2]))
- size_offset_pos = buf.seek (0, io.SEEK_CUR)
- buf.write (p ('<II', 0, 0))
- frame_offsets.append ([size_offset_pos])
-
- for i, frame in enumerate (frames):
- frame_offset = buf.seek (0, io.SEEK_CUR)
- frame_offsets[i].append (frame_offset)
-
- frame_png = Image.open (frame[3])
-
- if args.add_shadows:
- succeeded, shadowed = create_shadow (frame_png, args)
- if succeeded == 0:
- frame_png.close ()
- frame_png = shadowed
-
-# Windows 10 fails to read PNG-compressed cursors for some reason
-# and the information about storing PNG-compressed cursors is
-# sparse. This is why PNG compression is not used.
-# Previously this was conditional on cursor size (<= 48 to be uncompressed).
- compressed = False
-
-# On the other hand, Windows 10 refuses to read very large
-# uncompressed animated cursor files, but does accept
-# PNG-compressed animated cursors for some reason. Go figure.
- if animated:
- compressed = True
-
- if compressed:
- write_png (buf, frame, frame_png)
- else:
- write_cur (buf, frame, frame_png)
-
- frame_png.close ()
- frame_end = buf.seek (0, io.SEEK_CUR)
- frame_offsets[i].append (frame_end - frame_offset)
+def main():
+ parser = argparse.ArgumentParser(
+ description="Creates .ani or .cur files from separate images and input metadata.",
+ add_help=False,
+ )
+ parser.add_argument(
+ "-V",
+ "--version",
+ action="version",
+ version="{}-{}".format(program_name, program_version),
+ help="Display the version number and exit.",
+ )
+ parser.add_argument(
+ "-h", "-?", action="help", help="Display the usage message and exit."
+ )
+ parser.add_argument(
+ "-p",
+ "--prefix",
+ metavar="dir",
+ default=None,
+ help="Find cursor images in the directory specified by dir. If not specified, the current directory is used.",
+ )
+ parser.add_argument(
+ "-s",
+ "--add-shadows",
+ action="store_true",
+ dest="add_shadows",
+ default=False,
+ help="Generate shadows for cursors (disabled by default).",
+ )
+ parser.add_argument(
+ "-n",
+ "--no-shadows",
+ action="store_false",
+ dest="add_shadows",
+ default=False,
+ help="Do not generate shadows for cursors (put after --add-shadows to cancel its effect).",
+ )
+
+ shadows = parser.add_argument_group(
+ title="Shadow generation",
+ description="Only relevant when --add-shadows is given",
+ )
+
+ shadows.add_argument(
+ "-r",
+ "--right-shift",
+ metavar="%",
+ type=float,
+ default=9.375,
+ help="Shift shadow right by this percentage of the canvas size (default is 9.375).",
+ )
+ shadows.add_argument(
+ "-d",
+ "--down-shift",
+ metavar="%",
+ type=float,
+ default=3.125,
+ help="Shift shadow down by this percentage of the canvas size (default is 3.125).",
+ )
+ shadows.add_argument(
+ "-b",
+ "--blur",
+ metavar="%",
+ type=float,
+ default=3.125,
+ help="Blur radius, in percentage of the canvas size (default is 3.125, set to 0 to disable blurring).",
+ )
+ shadows.add_argument(
+ "-c",
+ "--color",
+ metavar="%",
+ default="0x00000040",
+ help="Shadow color in 0xRRGGBBAA form (default is 0x00000040).",
+ )
+
+ parser.add_argument(
+ "input_config",
+ default="-",
+ metavar="input-config [output-file]",
+ nargs="?",
+ help="Input config file (stdin by default).",
+ )
+ parser.add_argument(
+ "output_file",
+ default="-",
+ metavar="",
+ nargs="?",
+ help="Output cursor file (stdout by default).",
+ )
+
+ args = parser.parse_args()
- for frame_offset in frame_offsets:
- buf.seek (frame_offset[0])
- buf.write (p ('<II', frame_offset[2], frame_offset[1]))
+ try:
+ if (
+ args.color[0] != "0"
+ or args.color[1] not in ["x", "X"]
+ or len(args.color) != 10
+ ):
+ raise ValueError
+ args.color = (
+ int(args.color[2:4], 16),
+ int(args.color[4:6], 16),
+ int(args.color[6:8], 16),
+ int(args.color[8:10], 16),
+ )
+ except:
+ print("Can't parse the color '{}'".format(args.color), file=sys.stderr)
+ parser.print_help()
+ return 1
- return buf
+ if args.prefix is None:
+ args.prefix = os.getcwd()
-def make_framesets (frames):
- framesets = []
- sizes = set ()
+ if args.input_config == "-":
+ input_config = sys.stdin
+ else:
+ print(f"opening {args.input_config}")
+ input_config = open(args.input_config, "r")
- # This assumes that frames are sorted
- size = 0
- for i, frame in enumerate (frames):
- if size == 0 or frame[0] != size:
- size = frame[0]
- counter = 0
+ if args.output_file == "-":
+ output_file = sys.stdout
+ else:
+ output_file = open(args.output_file, "wb")
- if size in sizes:
- print ("Frames are not sorted: frame {} has size {}, but we have seen that already".format (i, size), file=sys.stderr)
- return None
+ result = make_cursor_from(input_config, output_file, args)
- sizes.add (size)
+ input_config.close()
+ output_file.close()
- if counter >= len (framesets):
- framesets.append ([])
+ return result
- framesets[counter].append (frame)
- counter += 1
- for i in range (1, len (framesets)):
- if len (framesets[i - 1]) != len (framesets[i]):
- print ("Frameset {} has size {}, expected {}".format (i, len (framesets[i]), len (framesets[i - 1])), file=sys.stderr)
- return None
+def make_cursor_from(inp, out, args):
+ frames = parse_config_from(inp, args.prefix)
- for frameset in framesets:
- for i in range (1, len (frameset)):
- if frameset[i - 1][4] != frameset[i][4]:
- print ("Frameset {} has duration {} for framesize {}, but {} for framesize {}".format (i, frameset[i][4], frameset[i][0], frameset[i - 1][4], frameset[i - 1][0]), file=sys.stderr)
- return None
+ animated = frames_have_animation(frames)
- def frameset_size_cmp (f1, f2):
- if f1[0][0] < f2[0][0]:
- return -1
- elif f1[0][0] > f2[0][0]:
- return 1
+ if animated:
+ result = make_ani(frames, out, args)
else:
- return 0
-
- framesets = sorted (framesets, frameset_size_cmp, reverse=True)
+ buf = make_cur(frames, args)
+ copy_to(out, buf)
+ result = 0
+
+ return result
- return framesets
-def make_ani (frames, out, args):
- framesets = make_framesets (frames)
- if framesets is None:
- return 1
+def copy_to(out, buf):
+ buf.seek(0, io.SEEK_SET)
+ while True:
+ b = buf.read(1024)
+ if len(b) == 0:
+ break
+ out.write(b)
+
+
+def frames_have_animation(frames):
+ sizes = set()
+
+ for frame in frames:
+ if frame[4] == 0:
+ continue
+ if frame[0] in sizes:
+ return True
+ sizes.add(frame[0])
+
+ return False
+
+
+def make_cur(frames, args, animated=False):
+ buf = io.BytesIO()
+ buf.write(p("<HHH", 0, 2, len(frames)))
+ frame_offsets = []
+
+ def frame_size_cmp(f1, f2):
+ if f1[0] < f2[0]:
+ return -1
+ elif f1[0] > f2[0]:
+ return 1
+ else:
+ return 0
+
+ frames = sorted(frames, key=functools.cmp_to_key(frame_size_cmp), reverse=True)
+
+ for frame in frames:
+ width = frame[0]
+ if width > 255:
+ width = 0
+ height = width
+ buf.write(p("<BBBB HH", width, height, 0, 0, frame[1], frame[2]))
+ size_offset_pos = buf.seek(0, io.SEEK_CUR)
+ buf.write(p("<II", 0, 0))
+ frame_offsets.append([size_offset_pos])
+
+ for i, frame in enumerate(frames):
+ frame_offset = buf.seek(0, io.SEEK_CUR)
+ frame_offsets[i].append(frame_offset)
+
+ frame_png = Image.open(frame[3])
+
+ if args.add_shadows:
+ succeeded, shadowed = create_shadow(frame_png, args)
+ if succeeded == 0:
+ frame_png.close()
+ frame_png = shadowed
+
+ # Windows 10 fails to read PNG-compressed cursors for some reason
+ # and the information about storing PNG-compressed cursors is
+ # sparse. This is why PNG compression is not used.
+ # Previously this was conditional on cursor size (<= 48 to be uncompressed).
+ compressed = False
+
+ # On the other hand, Windows 10 refuses to read very large
+ # uncompressed animated cursor files, but does accept
+ # PNG-compressed animated cursors for some reason. Go figure.
+ if animated:
+ compressed = True
+
+ if compressed:
+ write_png(buf, frame, frame_png)
+ else:
+ write_cur(buf, frame, frame_png)
+
+ frame_png.close()
+
+ frame_end = buf.seek(0, io.SEEK_CUR)
+ frame_offsets[i].append(frame_end - frame_offset)
+
+ for frame_offset in frame_offsets:
+ buf.seek(frame_offset[0])
+ buf.write(p("<II", frame_offset[2], frame_offset[1]))
+
+ return buf
+
+
+def make_framesets(frames):
+ framesets = []
+ sizes = set()
+
+ # This assumes that frames are sorted
+ size = 0
+ for i, frame in enumerate(frames):
+ if size == 0 or frame[0] != size:
+ size = frame[0]
+ counter = 0
+
+ if size in sizes:
+ print(
+ "Frames are not sorted: frame {} has size {}, but we have seen that already".format(
+ i, size
+ ),
+ file=sys.stderr,
+ )
+ return None
+
+ sizes.add(size)
+
+ if counter >= len(framesets):
+ framesets.append([])
+
+ framesets[counter].append(frame)
+ counter += 1
+
+ for i in range(1, len(framesets)):
+ if len(framesets[i - 1]) != len(framesets[i]):
+ print(
+ "Frameset {} has size {}, expected {}".format(
+ i, len(framesets[i]), len(framesets[i - 1])
+ ),
+ file=sys.stderr,
+ )
+ return None
- buf = io.BytesIO ()
+ for frameset in framesets:
+ for i in range(1, len(frameset)):
+ if frameset[i - 1][4] != frameset[i][4]:
+ print(
+ "Frameset {} has duration {} for framesize {}, but {} for framesize {}".format(
+ i,
+ frameset[i][4],
+ frameset[i][0],
+ frameset[i - 1][4],
+ frameset[i - 1][0],
+ ),
+ file=sys.stderr,
+ )
+ return None
+
+ def frameset_size_cmp(f1, f2):
+ if f1[0][0] < f2[0][0]:
+ return -1
+ elif f1[0][0] > f2[0][0]:
+ return 1
+ else:
+ return 0
+
+ framesets = sorted(framesets, key=functools.cmp_to_key(frameset_size_cmp), reverse=True)
+
+ return framesets
+
+
+def make_ani(frames, out, args):
+ framesets = make_framesets(frames)
+ if framesets is None:
+ return 1
+
+ buf = io.BytesIO()
+
+ buf.write(b"RIFF")
+ riff_len_pos = buf.seek(0, io.SEEK_CUR)
+ buf.write(p("<I", 0))
+ riff_len_start = buf.seek(0, io.SEEK_CUR)
+
+ buf.write(b"ACON")
+ buf.write(b"anih")
+ buf.write(
+ p(
+ "<IIIIIIIIII",
+ 36,
+ 36,
+ len(framesets),
+ len(framesets),
+ 0,
+ 0,
+ 32,
+ 1,
+ framesets[0][0][4],
+ 0x01,
+ )
+ )
+
+ rates = set()
+ for frameset in framesets:
+ rates.add(frameset[0][4])
- buf.write (b'RIFF')
- riff_len_pos = buf.seek (0, io.SEEK_CUR)
- buf.write (p ('<I', 0))
- riff_len_start = buf.seek (0, io.SEEK_CUR)
+ if len(rates) != 1:
+ buf.write(b"rate")
+ buf.write(p("<I", len(framesets) * 4))
+ for frameset in framesets:
+ buf.write(p("<I", frameset[0][4]))
- buf.write (b'ACON')
- buf.write (b'anih')
- buf.write (p ('<IIIIIIIIII', 36, 36, len (framesets), len (framesets), 0, 0, 32, 1, framesets[0][0][4], 0x01))
+ buf.write(b"LIST")
+ list_len_pos = buf.seek(0, io.SEEK_CUR)
+ buf.write(p("<I", 0))
+ list_len_start = buf.seek(0, io.SEEK_CUR)
- rates = set ()
- for frameset in framesets:
- rates.add (frameset[0][4])
+ buf.write(b"fram")
- if len (rates) != 1:
- buf.write (b'rate')
- buf.write (p ('<I', len (framesets) * 4))
for frameset in framesets:
- buf.write (p ('<I', frameset[0][4]))
-
- buf.write (b'LIST')
- list_len_pos = buf.seek (0, io.SEEK_CUR)
- buf.write (p ('<I', 0))
- list_len_start = buf.seek (0, io.SEEK_CUR)
-
- buf.write (b'fram')
-
- for frameset in framesets:
- buf.write (b'icon')
- cur = make_cur (frameset, args, animated=True)
- cur_size = cur.seek (0, io.SEEK_END)
- aligned_cur_size = cur_size
- #if cur_size % 4 != 0:
- # aligned_cur_size += 4 - cur_size % 2
- buf.write (p ('<i', cur_size))
- copy_to (buf, cur)
- pos = buf.seek (0, io.SEEK_END)
- if pos % 2 != 0:
- buf.write ('\x00' * (2 - (pos % 2)))
-
- end_at = buf.seek (0, io.SEEK_CUR)
- buf.seek (riff_len_pos, io.SEEK_SET)
- buf.write (p ('<I', end_at - riff_len_start))
- buf.seek (list_len_pos, io.SEEK_SET)
- buf.write (p ('<I', end_at - list_len_start))
-
- copy_to (out, buf)
-
- return 0
-
-def write_png (out, frame, frame_png):
- frame_png.save (out, "png", optimize=True)
-
-def write_cur (out, frame, frame_png):
- pixels = frame_png.load ()
-
- out.write (p ('<I II HH IIIIII', 40, frame[0], frame[0] * 2, 1, 32, 0, 0, 0, 0, 0, 0))
-
- for y in reversed (range (frame[0])):
- for x in range (frame[0]):
- pixel = pixels[x, y]
- out.write (p ('<BBBB', pixel[2], pixel[1], pixel[0], pixel[3]))
-
- acc = 0
- acc_pos = 0
- for y in reversed (range (frame[0])):
- wrote = 0
- for x in range (frame[0]):
- if pixels[x, y][3] <= 127:
- acc = acc | (1 << acc_pos)
- acc_pos += 1
- if acc_pos == 8:
- acc_pos = 0
- out.write (chr (acc))
- wrote += 1
- if wrote % 4 != 0:
- out.write (b'\x00' * (4 - wrote % 4))
-
-def parse_config_from (inp, prefix):
- frames = []
-
- for line in inp.readlines ():
- words = shlex.split (line.rstrip ('\n').rstrip ('\r'))
-
- if len (words) < 4:
- continue
-
- try:
- size = int (words[0])
- hotx = int (words[1]) - 1
- hoty = int (words[2]) - 1
- filename = words[3]
- if not os.path.isabs (filename):
- filename = prefix + '/' + filename
- except:
- continue
-
- if len (words) > 4:
- try:
- duration = int (words[4])
- except:
- continue
- else:
- duration = 0
-
- frames.append ((size, hotx, hoty, filename, duration))
-
- return frames
-
-def create_shadow (orig, args):
- blur_px = orig.size[0] / 100.0 * args.blur
- right_px = int (orig.size[0] / 100.0 * args.right_shift)
- down_px = int (orig.size[1] / 100.0 * args.down_shift)
-
- shadow = Image.new ('RGBA', orig.size, (0, 0, 0, 0))
- shadowize (shadow, orig, args.color)
- shadow.load ()
-
- if args.blur > 0:
- crop = (int (math.floor (-blur_px)), int (math.floor (-blur_px)), orig.size[0] + int (math.ceil (blur_px)), orig.size[1] + int (math.ceil (blur_px)))
- right_px += int (math.floor (-blur_px))
- down_px += int (math.floor (-blur_px))
- shadow = shadow.crop (crop)
- flt = ImageFilter.GaussianBlur (blur_px)
- shadow = shadow.filter (flt)
- shadow.load ()
-
- shadowed = Image.new ('RGBA', orig.size, (0, 0, 0, 0))
- shadowed.paste (shadow, (right_px, down_px))
- shadowed.crop ((0, 0, orig.size[0], orig.size[1]))
- shadowed = Image.alpha_composite (shadowed, orig)
-
- return 0, shadowed
-
-def shadowize (shadow, orig, color):
- o_pxs = orig.load ()
- s_pxs = shadow.load ()
- for y in range (orig.size[1]):
- for x in range (orig.size[0]):
- o_px = o_pxs[x, y]
- if o_px[3] > 0:
- s_pxs[x, y] = (color[0], color[1], color[2], int (color[3] * (o_px[3] / 255.0)))
-
-if __name__ == '__main__':
- sys.exit (main ())
+ buf.write(b"icon")
+ cur = make_cur(frameset, args, animated=True)
+ cur_size = cur.seek(0, io.SEEK_END)
+ aligned_cur_size = cur_size
+ # if cur_size % 4 != 0:
+ # aligned_cur_size += 4 - cur_size % 2
+ buf.write(p("<i", cur_size))
+ copy_to(buf, cur)
+ pos = buf.seek(0, io.SEEK_END)
+ if pos % 2 != 0:
+ buf.write(b"\x00" * (2 - (pos % 2)))
+
+ end_at = buf.seek(0, io.SEEK_CUR)
+ buf.seek(riff_len_pos, io.SEEK_SET)
+ buf.write(p("<I", end_at - riff_len_start))
+ buf.seek(list_len_pos, io.SEEK_SET)
+ buf.write(p("<I", end_at - list_len_start))
+
+ copy_to(out, buf)
+
+ return 0
+
+
+def write_png(out, frame, frame_png):
+ frame_png.save(out, "png", optimize=True)
+
+
+def write_cur(out, frame, frame_png):
+ pixels = frame_png.load()
+
+ out.write(p("<I II HH IIIIII", 40, frame[0], frame[0] * 2, 1, 32, 0, 0, 0, 0, 0, 0))
+
+ for y in reversed(range(frame[0])):
+ for x in range(frame[0]):
+ pixel = pixels[x, y]
+ out.write(p("<BBBB", pixel[2], pixel[1], pixel[0], pixel[3]))
+
+ acc = 0
+ acc_pos = 0
+ for y in reversed(range(frame[0])):
+ wrote = 0
+ for x in range(frame[0]):
+ if pixels[x, y][3] <= 127:
+ acc = acc | (1 << acc_pos)
+ acc_pos += 1
+ if acc_pos == 8:
+ acc_pos = 0
+ out.write(chr(acc).encode())
+ wrote += 1
+ if wrote % 4 != 0:
+ out.write(b"\x00" * (4 - wrote % 4))
+
+
+def parse_config_from(inp, prefix):
+ frames = []
+
+ for line in inp.readlines():
+ words = shlex.split(line.rstrip("\n").rstrip("\r"))
+
+ if len(words) < 4:
+ continue
+
+ try:
+ size = int(words[0])
+ hotx = int(words[1]) - 1
+ hoty = int(words[2]) - 1
+ filename = words[3]
+ if not os.path.isabs(filename):
+ filename = prefix + "/" + filename
+ except:
+ continue
+
+ if len(words) > 4:
+ try:
+ duration = int(words[4])
+ except:
+ continue
+ else:
+ duration = 0
+
+ frames.append((size, hotx, hoty, filename, duration))
+
+ return frames
+
+
+def create_shadow(orig, args):
+ blur_px = orig.size[0] / 100.0 * args.blur
+ right_px = int(orig.size[0] / 100.0 * args.right_shift)
+ down_px = int(orig.size[1] / 100.0 * args.down_shift)
+
+ shadow = Image.new("RGBA", orig.size, (0, 0, 0, 0))
+ shadowize(shadow, orig, args.color)
+ shadow.load()
+
+ if args.blur > 0:
+ crop = (
+ int(math.floor(-blur_px)),
+ int(math.floor(-blur_px)),
+ orig.size[0] + int(math.ceil(blur_px)),
+ orig.size[1] + int(math.ceil(blur_px)),
+ )
+ right_px += int(math.floor(-blur_px))
+ down_px += int(math.floor(-blur_px))
+ shadow = shadow.crop(crop)
+ flt = ImageFilter.GaussianBlur(blur_px)
+ shadow = shadow.filter(flt)
+ shadow.load()
+
+ shadowed = Image.new("RGBA", orig.size, (0, 0, 0, 0))
+ shadowed.paste(shadow, (right_px, down_px))
+ shadowed.crop((0, 0, orig.size[0], orig.size[1]))
+ shadowed = Image.alpha_composite(shadowed, orig)
+
+ return 0, shadowed
+
+
+def shadowize(shadow, orig, color):
+ o_pxs = orig.load()
+ s_pxs = shadow.load()
+ for y in range(orig.size[1]):
+ for x in range(orig.size[0]):
+ o_px = o_pxs[x, y]
+ if o_px[3] > 0:
+ s_pxs[x, y] = (
+ color[0],
+ color[1],
+ color[2],
+ int(color[3] * (o_px[3] / 255.0)),
+ )
+
+
+if __name__ == "__main__":
+ sys.exit(main())