#!/usr/bin/python3 # -*- coding: utf-8 -*- # anicursorgen # Copyright (C) 2015 Руслан Ижбулатов # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys import os import argparse 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_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: print(f"opening {args.input_config}") input_config = open(args.input_config, "r") 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(" 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("= 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 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(" 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())