summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Steiner <jimmac@gmail.com>2023-05-10 12:58:58 +0200
committerJakub Steiner <jimmac@gmail.com>2023-05-10 12:58:58 +0200
commit4d082e44e8361ca32360bb5b865be98a784ef4db (patch)
tree909ac1c4568617e48d107cda28c242a0f1d9048d
parent2db0e443fe79add6372ca51cde1bc61b21748823 (diff)
downloadadwaita-icon-theme-4d082e44e8361ca32360bb5b865be98a784ef4db.tar.gz
port cursor tooling to python3
Fixes https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/issues/234
-rwxr-xr-xsrc/cursors/anicursorgen.py804
-rwxr-xr-xsrc/cursors/renderpngs.py1523
2 files changed, 1321 insertions, 1006 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())
diff --git a/src/cursors/renderpngs.py b/src/cursors/renderpngs.py
index 6bef47e12..65a2d3ec1 100755
--- a/src/cursors/renderpngs.py
+++ b/src/cursors/renderpngs.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
#
# SVGSlice
#
@@ -27,22 +27,8 @@ Please remember to HIDE the slices layer before exporting, so that the rectangle
# all of the slices in once place, and perhaps a starting point for more layout work.
#
-from optparse import OptionParser
-
-optParser = OptionParser()
-optParser.add_option('-d','--debug',action='store_true',dest='debug',help='Enable extra debugging info.')
-optParser.add_option('-t','--test',action='store_true',dest='testing',help='Test mode: leave temporary files for examination.')
-optParser.add_option('-p','--sliceprefix',action='store',dest='sliceprefix',help='Specifies the prefix to use for individual slice filenames.')
-optParser.add_option('-r','--remove-shadows',action='store_true',dest='remove_shadows',help='Remove shadows the cursors have.')
-optParser.add_option('-o','--hotspots',action='store_true',dest='hotspots',help='Produce hotspot images and hotspot datafiles.')
-optParser.add_option('-s','--scales',action='store_true',dest='scales',help='Produce 125% (Large) and 150% (Extra Large) scaled versions of each image as well.')
-optParser.add_option('-m','--min-canvas-size',action='store',type='int',dest='min_canvas_size',default=-1, help='Cursor canvas must be at least this big (defaults to -1).')
-optParser.add_option('-f','--fps',action='store',type='int',dest='fps',default=60,help='Assume that all animated cursors have this FPS (defaults to 60).')
-optParser.add_option('-a','--anicursorgen',action='store_true',dest='anicur',default=False,help='Assume that anicursorgen will be used to assemble cursors (xcursorgen is assumed by default).')
-optParser.add_option('-c','--corner-align',action='store_true',dest='align_corner',default=False,help='Align cursors to the top-left corner (by default they are centered).')
-optParser.add_option('-i','--invert',action='store_true',dest='invert',default=False,help='Invert colors (disabled by default).')
-optParser.add_option('-n','--number-of-renderers',action='store',type='int',dest='number_of_renderers',default=1, help='Number of renderer instances run in parallel. Defaults to 1. Set to 0 for autodetection.')
-
+import argparse
+import logging
from xml.sax import saxutils, make_parser, SAXParseException, handler, xmlreader
from xml.sax.handler import feature_namespaces
import os, sys, tempfile, shutil, subprocess
@@ -52,675 +38,876 @@ from PIL import Image
import multiprocessing
import io
-svgFilename = None
-hotsvgFilename = None
-sizes = [24,32,48,64,96]
-scale_pairs = [(1.25, 's1'), (1.50, 's2')]
-mode_shadows = ['shadows']
-mode_hotspots = ['hotspots']
-mode_slices = ['slices']
-mode_invert = ['invert']
+
+MODE_HOTSPOTS = ["hotspots"]
+MODE_INVERT = ["invert"]
+MODE_SHADOWS = ["shadows"]
+MODE_SLICES = ["slices"]
+RENDERERS = []
+SCALE_PAIRS = [(1.25, "s1"), (1.50, "s2")]
+SIZES = [24, 32, 48, 64, 96]
+SVG_HOTSPOT_WORKING_COPY = "hotspot-working-copy.svg"
+SVG_WORKING_COPY = "working-copy.svg"
+
+debug = logging.debug
+error = logging.error
+info = logging.info
+warning = logging.warning
+
+
+def fatal(msg):
+ logging.critical(msg)
+ sys.exit(20)
+
+
+def configure():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("originalFilename", help="The input SVG file")
+ parser.add_argument(
+ "-d",
+ "--debug",
+ action="store_true",
+ dest="debug",
+ help="Enable extra debugging info.",
+ )
+ parser.add_argument(
+ "-t",
+ "--test",
+ action="store_true",
+ dest="testing",
+ help="Test mode: leave temporary files for examination.",
+ )
+ parser.add_argument(
+ "-p",
+ "--sliceprefix",
+ action="store",
+ dest="sliceprefix",
+ default="",
+ help="Specifies the prefix to use for individual slice filenames.",
+ )
+ parser.add_argument(
+ "-r",
+ "--remove-shadows",
+ action="store_true",
+ dest="remove_shadows",
+ help="Remove shadows the cursors have.",
+ )
+ parser.add_argument(
+ "-o",
+ "--hotspots",
+ action="store_true",
+ dest="hotspots",
+ help="Produce hotspot images and hotspot datafiles.",
+ )
+ parser.add_argument(
+ "-s",
+ "--scales",
+ action="store_true",
+ dest="scales",
+ help="Produce 125 and 150 percent (Large, and Extra Large) scaled versions of each image as well.",
+ )
+ parser.add_argument(
+ "-m",
+ "--min-canvas-size",
+ action="store",
+ type=int,
+ dest="min_canvas_size",
+ default=-1,
+ help="Cursor canvas must be at least this big (defaults to -1).",
+ )
+ parser.add_argument(
+ "-f",
+ "--fps",
+ action="store",
+ type=int,
+ dest="fps",
+ default=60,
+ help="Assume that all animated cursors have this FPS (defaults to 60).",
+ )
+ parser.add_argument(
+ "-a",
+ "--anicursorgen",
+ action="store_true",
+ dest="anicur",
+ default=False,
+ help="Assume that anicursorgen will be used to assemble cursors (xcursorgen is assumed by default).",
+ )
+ parser.add_argument(
+ "-c",
+ "--corner-align",
+ action="store_true",
+ dest="align_corner",
+ default=False,
+ help="Align cursors to the top-left corner (by default they are centered).",
+ )
+ parser.add_argument(
+ "-i",
+ "--invert",
+ action="store_true",
+ dest="invert",
+ default=False,
+ help="Invert colors (disabled by default).",
+ )
+ parser.add_argument(
+ "-n",
+ "--number-of-renderers",
+ action="store",
+ type=int,
+ dest="number_of_renderers",
+ default=1,
+ help="Number of renderer instances run in parallel. Defaults to 1. Set to 0 for autodetection.",
+ )
+
+ options = parser.parse_args()
+
+ # More detailed logging format if debug is enabled
+ if options.debug:
+ fmt = "[%(levelname)s] %(lineno)d:%(funcName)-s - %(message)s"
+ level = logging.DEBUG
+ else:
+ fmt = "[%(levelname)s] %(message)s"
+ level = logging.INFO
+ logging.basicConfig(level=level, format=fmt)
+
+ options.modes = get_modes(options)
+
+ if not options.scales:
+ del SCALE_PAIRS[:]
+
+ if options.number_of_renderers <= 0:
+ options.number_of_renderers = autodetect_threadcount()
+ return options
+
def natural_sort(l):
- convert = lambda text: int(text) if text.isdigit() else text.lower()
- alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ]
- return sorted(l, key = alphanum_key)
+ convert = lambda text: int(text) if text.isdigit() else text.lower()
+ alphanum_key = lambda key: [convert(c) for c in re.split("([0-9]+)", key)]
+ return sorted(l, key=alphanum_key)
-def dbg(msg):
- if options.debug:
- sys.stderr.write(msg)
def cleanup():
- global inkscape_instances
- stdin_threads = []
- for inkscape, inkscape_stderr, inkscape_stderr_thread, inkscape_stdin_buf in inkscape_instances:
- inkscape_stdin_buf.append ('quit\n')
- stdin_threads.append (Thread (target = stdin_writer, args=(inkscape, ''.join (inkscape_stdin_buf))))
- stdin_threads[-1].start ()
- inkscape_stderr_thread.start ()
- for t in stdin_threads:
- t.join ()
- del t
- for inkscape, inkscape_stderr, inkscape_stderr_thread, inkscape_stdin_buf in inkscape_instances:
- inkscape_stderr_thread.join ()
- del inkscape
- del inkscape_stderr_thread
- del inkscape_stderr
- del inkscape_stdin_buf
- del inkscape_instances
- del stdin_threads
- if svgFilename != None and os.path.exists(svgFilename):
- os.unlink(svgFilename)
- if hotsvgFilename != None and os.path.exists(hotsvgFilename):
- os.unlink(hotsvgFilename)
-
-def fatalError(msg):
- sys.stderr.write(msg)
- cleanup()
- sys.exit(20)
+ global RENDERERS
+ for inkscape, inkscape_stderr, inkscape_stderr_thread in RENDERERS:
+ inkscape.communicate("quit\n".encode())
+ del inkscape
+ del inkscape_stderr_thread
+ del inkscape_stderr
+ del RENDERERS
+
+ if SVG_WORKING_COPY != None and os.path.exists(SVG_WORKING_COPY):
+ os.remove(SVG_WORKING_COPY)
+
+ if SVG_HOTSPOT_WORKING_COPY != None and os.path.exists(SVG_HOTSPOT_WORKING_COPY):
+ os.remove(SVG_HOTSPOT_WORKING_COPY)
+
+
+def find_hotspot(hotfile):
+ img = Image.open(hotfile)
+ pixels = img.load()
+ reddest = [-1, -1, -999999]
+ for y in range(img.size[1]):
+ for x in range(img.size[0]):
+ redness = pixels[x, y][0] - pixels[x, y][1] - pixels[x, y][2]
+ if redness > reddest[2]:
+ reddest = [x, y, redness]
+ return (reddest[0] + 1, reddest[1] + 1)
+
+
+def cropalign(size, filename):
+ img = Image.open(filename)
+ content_dimensions = img.getbbox()
+ if content_dimensions is None:
+ content_dimensions = (0, 0, img.size[0], img.size[1])
+ hcropped = content_dimensions[2] - content_dimensions[0]
+ vcropped = content_dimensions[3] - content_dimensions[1]
+ if hcropped > size or vcropped > size:
+ if hcropped > size:
+ left = (hcropped - size) / 2
+ right = (hcropped - size) - left
+ else:
+ left = 0
+ right = 0
+ if vcropped > size:
+ top = (vcropped - size) / 2
+ bottom = (vcropped - size) - top
+ else:
+ top = 0
+ bottom = 0
+ content_dimensions = (
+ content_dimensions[0] + left,
+ content_dimensions[1] + top,
+ content_dimensions[2] - right,
+ content_dimensions[3] - bottom,
+ )
+ warn(
+ f"{filename} is too big to be cleanly cropped to {size} ({hcropped}x{vcropped} at best)"
+ )
+ warn(
+ "cropping to {}x{}!".format(
+ content_dimensions[2] - content_dimensions[0],
+ content_dimensions[3] - content_dimensions[1],
+ )
+ )
+
+ if options.testing:
+ img.save(filename + ".orig.png", "png")
+
+ debug(
+ f"{filename} content is {content_dimensions[0]} {content_dimensions[1]} {content_dimensions[2]} {content_dimensions[3]}"
+ )
+
+ cropimg = img.crop(
+ (
+ content_dimensions[0],
+ content_dimensions[1],
+ content_dimensions[2],
+ content_dimensions[3],
+ )
+ )
+ pixels = cropimg.load()
+ if options.testing:
+ cropimg.save(filename + ".crop.png", "png")
+ if options.align_corner:
+ expimg = cropimg.crop((0, 0, size, size))
+ result = (content_dimensions[0], content_dimensions[1])
+ else:
+ hslack = size - cropimg.size[0]
+ vslack = size - cropimg.size[1]
+ left = hslack / 2
+ top = vslack / 2
+ expimg = cropimg.crop((-left, -top, size - left, size - top))
+ result = (content_dimensions[0] - left, content_dimensions[1] - top)
+ pixels = expimg.load()
+ if options.invert:
+ negative(expimg)
+ expimg.save(filename, "png")
+ del cropimg
+ del img
+ return result
+
+
+def cropalign_hotspot(new_base, size, filename):
+ if not new_base:
+ return
+ img = Image.open(filename)
+ expimg = img.crop(
+ (new_base[0], new_base[1], new_base[0] + size, new_base[1] + size)
+ )
+ pixels = expimg.load()
+ expimg.save(filename, "png")
+ del img
+
+
+def negative(img):
+ pixels = img.load()
+ for y in range(0, img.size[1]):
+ for x in range(0, img.size[0]):
+ r, g, b, a = pixels[x, y]
+ pixels[x, y] = (255 - r, 255 - g, 255 - b, a)
-def stderr_reader(inkscape, inkscape_stderr):
- while True:
- line = inkscape_stderr.readline()
- if line and len (line.rstrip ('\n').rstrip ('\r')) > 0:
- fatalError('ABORTING: Inkscape failed to render a slice: {}'.format (line))
- elif line:
- print "STDERR> {}".format (line)
- else:
- raise EOFError
-
-def stdin_writer(inkscape, inkscape_stdin):
- inkscape.stdin.write (inkscape_stdin)
-
-def find_hotspot (hotfile):
- img = Image.open(hotfile)
- pixels = img.load()
- reddest = [-1, -1, -999999]
- for y in range(img.size[1]):
- for x in range(img.size[0]):
- redness = pixels[x,y][0] - pixels[x,y][1] - pixels[x,y][2]
- if redness > reddest[2]:
- reddest = [x, y, redness]
- return (reddest[0] + 1, reddest[1] + 1)
-
-def cropalign (size, filename):
- img = Image.open (filename)
- content_dimensions = img.getbbox ()
- if content_dimensions is None:
- content_dimensions = (0, 0, img.size[0], img.size[1])
- hcropped = content_dimensions[2] - content_dimensions[0]
- vcropped = content_dimensions[3] - content_dimensions[1]
- if hcropped > size or vcropped > size:
- if hcropped > size:
- left = (hcropped - size) / 2
- right = (hcropped - size) - left
- else:
- left = 0
- right = 0
- if vcropped > size:
- top = (vcropped - size) / 2
- bottom = (vcropped - size) - top
- else:
- top = 0
- bottom = 0
- content_dimensions = (content_dimensions[0] + left, content_dimensions[1] + top, content_dimensions[2] - right, content_dimensions[3] - bottom)
- sys.stderr.write ("WARNING: {} is too big to be cleanly cropped to {} ({}x{} at best), cropping to {}x{}!\n".format (filename, size, hcropped, vcropped, content_dimensions[2] - content_dimensions[0], content_dimensions[3] - content_dimensions[1]))
- sys.stderr.flush ()
- if options.testing:
- img.save (filename + ".orig.png", "png")
- dbg("{} content is {} {} {} {}".format (filename, content_dimensions[0], content_dimensions[1], content_dimensions[2], content_dimensions[3]))
- cropimg = img.crop ((content_dimensions[0], content_dimensions[1], content_dimensions[2], content_dimensions[3]))
- pixels = cropimg.load ()
- if options.testing:
- cropimg.save (filename + ".crop.png", "png")
- if options.align_corner:
- expimg = cropimg.crop ((0, 0, size, size))
- result = (content_dimensions[0], content_dimensions[1])
- else:
- hslack = size - cropimg.size[0]
- vslack = size - cropimg.size[1]
- left = hslack / 2
- top = vslack / 2
- expimg = cropimg.crop ((-left, -top, size - left, size - top))
- result = (content_dimensions[0] - left, content_dimensions[1] - top)
- pixels = expimg.load ()
- if options.invert:
- negative (expimg)
- expimg.save (filename, "png")
- del cropimg
- del img
- return result
-
-def cropalign_hotspot (new_base, size, filename):
- if new_base is None:
- return
- img = Image.open (filename)
- expimg = img.crop ((new_base[0], new_base[1], new_base[0] + size, new_base[1] + size))
- pixels = expimg.load ()
- expimg.save (filename, "png")
- del img
-
-def negative (img):
- pixels = img.load ()
- for y in range (0, img.size[1]):
- for x in range (0, img.size[0]):
- r, g, b, a = pixels[x,y]
- pixels[x,y] = (255 - r, 255 - g, 255 - b, a)
class SVGRect:
- """Manages a simple rectangular area, along with certain attributes such as a name"""
- def __init__(self, x1,y1,x2,y2, name=None):
- self.x1 = x1
- self.y1 = y1
- self.x2 = x2
- self.y2 = y2
- self.name = name
- dbg("New SVGRect: (%s)" % name)
-
- def renderFromSVG(self, svgFName, slicename, skipped, roundrobin, hotsvgFName):
-
- def do_res (size, output, svgFName, skipped, roundrobin):
- global inkscape_instances
- if os.path.exists (output):
- skipped[output] = True
- return
- command = '-w {size} -h {size} --export-id="{export_id}" --export-png="{export_png}" {svg}\n'.format (size=size, export_id=self.name, export_png=output, svg=svgFName)
- dbg("Command: {}".format (command))
- inkscape_instances[roundrobin[0]][3].append (command)
-
- pngsliceFName = slicename + '.png'
- hotsliceFName = slicename + '.hotspot.png'
-
- dbg('Saving slice as: "%s"' % pngsliceFName)
- for i, size in enumerate (sizes):
- subdir = 'pngs/{}x{}'.format (size, size)
- if not os.path.exists (subdir):
- os.makedirs (subdir)
- relslice = '{}/{}'.format (subdir, pngsliceFName)
- do_res (size, relslice, svgFName, skipped, roundrobin)
- if options.hotspots:
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- do_res (size, hotrelslice, hotsvgFName, skipped, roundrobin)
- for scale in scale_pairs:
- subdir = 'pngs/{}x{}_{}'.format (size, size, scale[1])
- relslice = '{}/{}'.format (subdir, pngsliceFName)
- if not os.path.exists (subdir):
- os.makedirs (subdir)
- scaled_size = int (size * scale[0])
- do_res (scaled_size, relslice, svgFName, skipped, roundrobin)
- if options.hotspots:
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- do_res (scaled_size, hotrelslice, hotsvgFName, skipped, roundrobin)
- # This is not inside do_res() because we want each instance to work all scales in case scales are enabled,
- # otherwise instances that get mostly smallscale renders will finish up way before the others
- roundrobin[0] += 1
- if roundrobin[0] >= options.number_of_renderers:
- roundrobin[0] = 0
-
-def get_next_size (index, current_size):
- if index % 2 == 0:
- # 24->32, 48->64, 96->128, 192->256
- return (current_size * 4) / 3
- else:
- # 32->48, 64->96, 128->192, 256->384
- return (current_size * 3) / 2
-
-def get_csize (index, current_size):
- size = current_size
- if len (scale_pairs) > 0:
- size = get_next_size (index, size)
- return max (options.min_canvas_size, size)
-
-def postprocess_slice (slicename, skipped):
- pngsliceFName = slicename + '.png'
- hotsliceFName = slicename + '.hotspot.png'
-
- for i, size in enumerate (sizes):
- subdir = 'pngs/{}x{}'.format (size, size)
- relslice = '{}/{}'.format (subdir, pngsliceFName)
- csize = get_csize (i, size)
- if relslice not in skipped:
- new_base = cropalign (csize, relslice)
- if options.hotspots:
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- cropalign_hotspot (new_base, csize, hotrelslice)
- for scale in scale_pairs:
- subdir = 'pngs/{}x{}_{}'.format (size, size, scale[1])
- relslice = '{}/{}'.format (subdir, pngsliceFName)
- if relslice not in skipped:
- new_base = cropalign (csize, relslice)
- if options.hotspots:
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- cropalign_hotspot (new_base, csize, hotrelslice)
+ """Manages a simple rectangular area, along with certain attributes such as a name"""
+
+ def __init__(self, x1, y1, x2, y2, name=None):
+ self.x1 = x1
+ self.y1 = y1
+ self.x2 = x2
+ self.y2 = y2
+ self.name = name
+ debug(f"New SVGRect: {name}")
+
+ def renderFromSVG(self, svgFName, slicename, skipped, roundrobin, hotsvgFName):
+ def do_res(size, output, svgFName):
+ global RENDERERS
+ nonlocal skipped, roundrobin
+ if os.path.exists(output):
+ debug(f"{output} exists, skip rendering")
+ skipped[output] = True
+ return
+
+ debug(f"rendering {output}")
+ command = f"export-width:{size};"
+ command += f" export-height:{size};"
+ command += f" export-id:{self.name};"
+ command += f" export-filename:{output};"
+ command += f" export-do\n"
+ debug(f"inkscape command: {command}")
+ RENDERERS[roundrobin[0]][0].stdin.write(command.encode())
+
+ pngsliceFName = f"{slicename}.png"
+ hotsliceFName = f"{slicename}.hotspot.png"
+
+ for i, size in enumerate(SIZES):
+ subdir = f"bitmaps/{size}x{size}"
+
+ if not os.path.exists(subdir):
+ os.makedirs(subdir)
+
+ relslice = f"{subdir}/{pngsliceFName}"
+ do_res(size, relslice, svgFName)
+
+ if options.hotspots:
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+ do_res(size, hotrelslice, hotsvgFName)
+
+ for scale in SCALE_PAIRS:
+ subdir = f"bitmaps/{size}x{size}_{scale[1]}"
+ relslice = f"{subdir}/{pngsliceFName}"
+
+ if not os.path.exists(subdir):
+ os.makedirs(subdir)
+
+ scaled_size = int(size * scale[0])
+ do_res(scaled_size, relslice, svgFName)
+
+ if options.hotspots:
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+ do_res(scaled_size, hotrelslice, hotsvgFName)
+
+ # This is not inside do_res() because we want each instance to work all scales in case scales are enabled,
+ # otherwise instances that get mostly smallscale renders will finish up way before the others
+ roundrobin[0] += 1
+ if roundrobin[0] >= options.number_of_renderers:
+ roundrobin[0] = 0
+
+
+def get_next_size(index, current_size):
+ if index % 2 == 0:
+ # 24->32, 48->64, 96->128, 192->256
+ return (current_size * 4) / 3
+ else:
+ # 32->48, 64->96, 128->192, 256->384
+ return (current_size * 3) / 2
+
+
+def get_csize(index, current_size):
+ size = current_size
+ if len(SCALE_PAIRS) > 0:
+ size = get_next_size(index, size)
+ return max(options.min_canvas_size, size)
+
+
+def postprocess_slice(slicename, skipped):
+ pngsliceFName = f"{slicename}.png"
+ hotsliceFName = f"{slicename}.hotspot.png"
+
+ for i, size in enumerate(SIZES):
+ subdir = f"bitmaps/{size}x{size}"
+ relslice = f"{subdir}/{pngsliceFName}"
+ csize = get_csize(i, size)
+ if relslice not in skipped:
+ if options.hotspots:
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+ for scale in SCALE_PAIRS:
+ subdir = f"bitmaps/{size}x{size}_{scale[1]}"
+ relslice = f"{subdir}/{pngsliceFName}"
+ if relslice not in skipped:
+ if options.hotspots:
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+
def write_xcur(slicename):
- pngsliceFName = slicename + '.png'
- hotsliceFName = slicename + '.hotspot.png'
-
- framenum = -1
- if slicename[-5:].startswith ('_'):
- try:
- framenum = int (slicename[-4:])
- slicename = slicename[:-5]
- except:
- pass
-
- # This relies on the fact that frame 1 is the first frame of an animation in the rect list
- # If that is not so, the *icongen input file will end up missing some of the lines
- if framenum == -1 or framenum == 1:
- mode = 'wb'
- else:
- mode = 'ab'
- if framenum == -1:
- fps_field = ''
- else:
- if options.anicur:
- # For anicursorgen use jiffies
- fps_field = ' {}'.format (int (60.0 / options.fps))
- else:
- # For xcursorgen use milliseconds
- fps_field = ' {}'.format (int (1000.0 / options.fps))
- xcur = {}
- xcur['s0'] = open ('pngs/{}.in'.format (slicename), mode)
- if len (scale_pairs) > 0:
- xcur['s1'] = open ('pngs/{}.s1.in'.format (slicename), mode)
- xcur['s2'] = open ('pngs/{}.s2.in'.format (slicename), mode)
- for i, size in enumerate (sizes):
- subdir = 'pngs/{}x{}'.format (size, size)
- relslice = '{}/{}'.format (subdir, pngsliceFName)
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- hot = find_hotspot (hotrelslice)
- csize = get_csize (i, size)
- xcur['s0'].write ("{csize} {hotx} {hoty} {filename}{fps_field}\n".format (csize=csize, hotx=hot[0], hoty=hot[1], filename='{}x{}/{}'.format (size, size, pngsliceFName), fps_field=fps_field))
- for scale in scale_pairs:
- subdir = 'pngs/{}x{}_{}'.format (size, size, scale[1])
- relslice = '{}/{}'.format (subdir, pngsliceFName)
- scaled_size = int (size * scale[0])
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- hot = find_hotspot (hotrelslice)
- xcur[scale[1]].write ("{csize} {hotx} {hoty} {filename}{fps_field}\n".format (csize=csize, hotx=hot[0], hoty=hot[1], filename='{}x{}_{}/{}'.format (size, size, scale[1], pngsliceFName), fps_field=fps_field))
- xcur['s0'].close ()
- if len (scale_pairs) > 0:
- xcur['s1'].close ()
- xcur['s2'].close ()
+ pngsliceFName = f"{slicename}.png"
+ hotsliceFName = f"{slicename}.hotspot.png"
+
+ framenum = -1
+ if slicename[-5:].startswith("_"):
+ try:
+ framenum = int(slicename[-4:])
+ slicename = slicename[:-5]
+ except:
+ pass
+
+ # This relies on the fact that frame 1 is the first frame of an animation in the rect list
+ # If that is not so, the *icongen input file will end up missing some of the lines
+ if framenum == -1 or framenum == 1:
+ mode = "wb"
+ else:
+ mode = "ab"
+ if framenum == -1:
+ fps_field = ""
+ else:
+ if options.anicur:
+ # For anicursorgen use jiffies
+ fps_field = " {}".format(int(60.0 / options.fps))
+ else:
+ # For xcursorgen use milliseconds
+ fps_field = " {}".format(int(1000.0 / options.fps))
+
+ xcur = {}
+ xcur["s0"] = open(f"bitmaps/{slicename}.in", mode)
+ if len(SCALE_PAIRS) > 0:
+ xcur["s1"] = open(f"bitmaps/{slicename}.s1.in", mode)
+ xcur["s2"] = open(f"bitmaps/{slicename}.s2.in", mode)
+
+ for i, size in enumerate(SIZES):
+ subdir = f"bitmaps/{size}x{size}"
+ relslice = f"{subdir}/{pngsliceFName}"
+ filename = f"{size}x{size}/{pngsliceFName}"
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+
+ hot = find_hotspot(hotrelslice)
+ csize = get_csize(i, size)
+
+ xcur["s0"].write(f"{csize} {hot[0]} {hot[1]} {filename}{fps_field}\n")
+
+ for scale in SCALE_PAIRS:
+ subdir = f"bitmaps/{size}x{size}_{scale[1]}"
+ relslice = f"{subdir}/{pngsliceFName}"
+ filename = f"{size}x{size}/{scale[1]}"
+ scaled_size = int(size * scale[0])
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+
+ hot = find_hotspot(hotrelslice)
+
+ xcur[scale[1]].write(f"{csize} {hot[0]} {hot[1]} {filename}{fps_field}\n")
+
+ xcur["s0"].close()
+ if len(SCALE_PAIRS) > 0:
+ xcur["s1"].close()
+ xcur["s2"].close()
+
def sort_file(filename):
- with open (filename, 'rb') as src:
- contents = src.readlines ()
- with open (filename, 'wb') as dst:
- for line in natural_sort (contents):
- dst.write (line)
+ with open(filename, "rb") as src:
+ contents = src.readlines()
+ with open(filename, "wb") as dst:
+ for line in natural_sort(contents):
+ dst.write(line)
+
def sort_xcur(slicename, passed):
- pngsliceFName = slicename + '.png'
-
- framenum = -1
- if slicename[-5:].startswith ('_'):
- try:
- framenum = int (slicename[-4:])
- slicename = slicename[:-5]
- except:
- pass
- if slicename in passed:
- return
- passed[slicename] = True
-
- sort_file ('pngs/{}.in'.format (slicename))
- if len (scale_pairs) > 0:
- sort_file ('pngs/{}.s1.in'.format (slicename))
- sort_file ('pngs/{}.s2.in'.format (slicename))
+ pngsliceFName = f"{slicename}.png"
+
+ framenum = -1
+ if slicename[-5:].startswith("_"):
+ try:
+ framenum = int(slicename[-4:])
+ slicename = slicename[:-5]
+ except:
+ pass
+ if slicename in passed:
+ return
+ passed[slicename] = True
+
+ sort_file("bitmaps/{slicename}.in")
+ if len(SCALE_PAIRS) > 0:
+ sort_file(f"bitmaps/{slicename}.s1.in")
+ sort_file(f"bitmaps/{slicename}.s2.in")
+
def delete_hotspot(slicename):
- hotsliceFName = slicename + '.hotspot.png'
-
- for i, size in enumerate (sizes):
- subdir = 'pngs/{}x{}'.format (size, size)
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- if os.path.exists (hotrelslice):
- os.unlink (hotrelslice)
- for scale in scale_pairs:
- subdir = 'pngs/{}x{}_{}'.format (size, size, scale[1])
- hotrelslice = '{}/{}'.format (subdir, hotsliceFName)
- if os.path.exists (hotrelslice):
- os.unlink (hotrelslice)
+ hotsliceFName = f"{slicename}.hotspot.png"
+
+ for i, size in enumerate(SIZES):
+ subdir = f"bitmaps/{size}x{size}"
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+ if os.path.exists(hotrelslice):
+ os.unlink(hotrelslice)
+ for scale in SCALE_PAIRS:
+ subdir = f"bitmaps/{size}x{size}_{scale[1]}"
+ hotrelslice = f"{subdir}/{hotsliceFName}"
+ if os.path.exists(hotrelslice):
+ os.unlink(hotrelslice)
+
class SVGHandler(handler.ContentHandler):
- """Base class for SVG parsers"""
- def __init__(self):
- self.pageBounds = SVGRect(0,0,0,0)
-
- def isFloat(self, stringVal):
- try:
- return (float(stringVal), True)[1]
- except (ValueError, TypeError), e:
- return False
-
- def parseCoordinates(self, val):
- """Strips the units from a coordinate, and returns just the value."""
- if val.endswith('px'):
- val = float(val.rstrip('px'))
- elif val.endswith('pt'):
- val = float(val.rstrip('pt'))
- elif val.endswith('cm'):
- val = float(val.rstrip('cm'))
- elif val.endswith('mm'):
- val = float(val.rstrip('mm'))
- elif val.endswith('in'):
- val = float(val.rstrip('in'))
- elif val.endswith('%'):
- val = float(val.rstrip('%'))
- elif self.isFloat(val):
- val = float(val)
- else:
- fatalError("Coordinate value %s has unrecognised units. Only px,pt,cm,mm,and in units are currently supported." % val)
- return val
-
- def startElement_svg(self, name, attrs):
- """Callback hook which handles the start of an svg image"""
- dbg('startElement_svg called')
- width = attrs.get('width', None)
- height = attrs.get('height', None)
- self.pageBounds.x2 = self.parseCoordinates(width)
- self.pageBounds.y2 = self.parseCoordinates(height)
-
- def endElement(self, name):
- """General callback for the end of a tag"""
- dbg('Ending element "%s"' % name)
+ """Base class for SVG parsers"""
+
+ def __init__(self):
+ self.pageBounds = SVGRect(0, 0, 0, 0)
+
+ def isFloat(self, stringVal):
+ try:
+ return (float(stringVal), True)[1]
+ except (ValueError, TypeError) as e:
+ return False
+
+ def parseCoordinates(self, val):
+ """Strips the units from a coordinate, and returns just the value."""
+
+ if self.isFloat(val):
+ return float(val)
+
+ res = None
+ supported_units = "px, pt, cm, mm, in, %"
+ for unit in supported_units.split(", "):
+ if val.endswith(unit):
+ res = float(val.rstrip(unit))
+ break
+
+ if not res:
+ fatal(
+ f"Unsupported unit in value {val}. Valid units are {supported_units}."
+ )
+
+ return res
+
+ def startElement_svg(self, name, attrs):
+ """Callback hook which handles the start of an svg image"""
+
+ width = attrs.get("width", None)
+ height = attrs.get("height", None)
+ self.pageBounds.x2 = self.parseCoordinates(width)
+ self.pageBounds.y2 = self.parseCoordinates(height)
+
+ def endElement(self, name):
+ """General callback for the end of a tag"""
+ debug(f'Ending element "{name}"')
class SVGLayerHandler(SVGHandler):
- """Parses an SVG file, extracing slicing rectangles from a "slices" layer"""
- def __init__(self):
- SVGHandler.__init__(self)
- self.svg_rects = []
- self.layer_nests = 0
-
- def inSlicesLayer(self):
- return (self.layer_nests >= 1)
-
- def add(self, rect):
- """Adds the given rect to the list of rectangles successfully parsed"""
- self.svg_rects.append(rect)
-
- def startElement_layer(self, name, attrs):
- """Callback hook for parsing layer elements
-
- Checks to see if we're starting to parse a slices layer, and sets the appropriate flags. Otherwise, the layer will simply be ignored."""
- dbg('found layer: name="%s" id="%s"' % (name, attrs['id']))
- if attrs.get('inkscape:groupmode', None) == 'layer':
- if self.inSlicesLayer() or attrs['inkscape:label'] == 'slices':
- self.layer_nests += 1
-
- def endElement_layer(self, name):
- """Callback for leaving a layer in the SVG file
-
- Just undoes any flags set previously."""
- dbg('leaving layer: name="%s"' % name)
- if self.inSlicesLayer():
- self.layer_nests -= 1
-
- def startElement_rect(self, name, attrs):
- """Callback for parsing an SVG rectangle
-
- Checks if we're currently in a special "slices" layer using flags set by startElement_layer(). If we are, the current rectangle is considered to be a slice, and is added to the list of parsed
- rectangles. Otherwise, it will be ignored."""
- if self.inSlicesLayer():
- x1 = self.parseCoordinates(attrs['x'])
- y1 = self.parseCoordinates(attrs['y'])
- x2 = self.parseCoordinates(attrs['width']) + x1
- y2 = self.parseCoordinates(attrs['height']) + y1
- name = attrs['id']
- rect = SVGRect(x1,y1, x2,y2, name)
- self.add(rect)
-
- def startElement(self, name, attrs):
- """Generic hook for examining and/or parsing all SVG tags"""
- dbg('Beginning element "%s"' % name)
- if name == 'svg':
- self.startElement_svg(name, attrs)
- elif name == 'g':
- # inkscape layers are groups, I guess, hence 'g'
- self.startElement_layer(name, attrs)
- elif name == 'rect':
- self.startElement_rect(name, attrs)
-
- def endElement(self, name):
- """Generic hook called when the parser is leaving each SVG tag"""
- dbg('Ending element "%s"' % name)
- if name == 'g':
- self.endElement_layer(name)
-
- def generateXHTMLPage(self):
- """Generates an XHTML page for the SVG rectangles previously parsed."""
- write = sys.stdout.write
- write('<?xml version="1.0" encoding="UTF-8"?>\n')
- write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">\n')
- write('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">\n')
- write(' <head>\n')
- write(' <title>Sample SVGSlice Output</title>\n')
- write(' </head>\n')
- write(' <body>\n')
- write(' <p>Sorry, SVGSlice\'s XHTML output is currently very basic. Hopefully, it will serve as a quick way to preview all generated slices in your browser, and perhaps as a starting point for further layout work. Feel free to write it and submit a patch to the author :)</p>\n')
-
- write(' <p>')
- for rect in self.svg_rects:
- write(' <img src="%s" alt="%s (please add real alternative text for this image)" longdesc="Please add a full description of this image" />\n' % (sliceprefix + rect.name + '.png', rect.name))
- write(' </p>')
-
- write('<p><a href="http://validator.w3.org/check?uri=referer"><img src="http://www.w3.org/Icons/valid-xhtml10" alt="Valid XHTML 1.0!" height="31" width="88" /></a></p>')
-
- write(' </body>\n')
- write('</html>\n')
-
-class SVGFilter (saxutils.XMLFilterBase):
- def __init__ (self, upstream, downstream, mode, **kwargs):
- saxutils.XMLFilterBase.__init__(self, upstream)
- self._downstream = downstream
- self.mode = mode
-
- def startDocument (self):
- self.in_throwaway_layer_stack = [False]
-
- def startElement (self, localname, attrs):
- def modify_style (style, old_style, new_style=None):
- styles = style.split (';')
- new_styles = []
- if old_style is not None:
- match_to = old_style + ':'
- for s in styles:
- if len (s) > 0 and (old_style is None or not s.startswith (match_to)):
- new_styles.append (s)
- if new_style is not None:
- new_styles.append (new_style)
- return ';'.join (new_styles)
-
- dict = {}
- is_throwaway_layer = False
- is_slices = False
- is_hotspots = False
- is_shadows = False
- is_layer = False
- if localname == 'g':
- for key, value in attrs.items ():
- if key == 'inkscape:label':
- if value == 'slices':
- is_slices = True
- elif value == 'hotspots':
- is_hotspots = True
- elif value == 'shadows':
- is_shadows = True
- elif key == 'inkscape:groupmode':
- if value == 'layer':
- is_layer = True
- if mode_shadows in self.mode and is_shadows:
- # Only remove the shadows
- is_throwaway_layer = True
- elif mode_hotspots in self.mode and not (is_hotspots or is_slices):
- # Remove all layers but hotspots and slices
- if localname == 'g':
- is_throwaway_layer = True
- idict = {}
- idict.update (attrs)
- if 'style' not in attrs.keys ():
- idict['style'] = ''
- for key, value in idict.items():
- alocalname = key
- if alocalname == 'style':
- had_style = True
- if alocalname == 'style' and is_slices:
- # Make slices invisible. Do not check the mode, because there is
- # no circumstances where we *want* to render slices
- value = modify_style (value, 'display', 'display:none')
- if alocalname == 'style' and is_hotspots:
- if mode_hotspots in self.mode:
- # Make hotspots visible in hotspots mode
- value = modify_style (value, 'display', 'display:inline')
- else:
- # Make hotspots invisible otherwise
- value = modify_style (value, 'display', 'display:none')
- if alocalname == 'style' and mode_invert in self.mode and is_layer and is_shadows:
- value = modify_style (value, None, 'filter:url(#InvertFilter)')
- dict[key] = value
-
- if self.in_throwaway_layer_stack[0] or is_throwaway_layer:
- self.in_throwaway_layer_stack.insert(0, True)
- else:
- self.in_throwaway_layer_stack.insert(0, False)
- attrs = xmlreader.AttributesImpl(dict)
- self._downstream.startElement(localname, attrs)
-
- def characters(self, content):
- if self.in_throwaway_layer_stack[0]:
- return
- self._downstream.characters(content)
-
- def endElement(self, localname):
- if self.in_throwaway_layer_stack.pop(0):
- return
- self._downstream.endElement(localname)
-
-def filter_svg (input, output, mode):
- """filter_svg(input:file, output:file, mode)
-
- Parses the SVG input from the input stream.
- For mode == 'hotspots' it filters out all
- layers except for hotspots and slices. Also makes hotspots
- visible.
- For mode == 'shadows' it filters out the shadows layer.
- """
-
- mode_objs = []
- if 'hotspots' in mode:
- mode_objs.append (mode_hotspots)
- if 'shadows' in mode:
- mode_objs.append (mode_shadows)
- if 'slices' in mode:
- mode_objs.append (mode_slices)
- if 'invert' in mode:
- mode_objs.append (mode_invert)
- if len (mode_objs) == 0:
- raise ValueError()
-
- output_gen = saxutils.XMLGenerator(output)
- parser = make_parser()
- filter = SVGFilter(parser, output_gen, mode_objs)
- filter.setFeature(handler.feature_namespaces, False)
- filter.setErrorHandler(handler.ErrorHandler())
- # This little I/O dance is here to ensure that SAX parser does not stash away
- # an open file descriptor for the input file, which would prevent us from unlinking it later
- with open (input, 'rb') as inp:
- contents = inp.read ()
- contents_io = io.BytesIO (contents)
- source_object = saxutils.prepare_input_source (contents_io)
- filter.parse(source_object)
- del filter
- del parser
- del output_gen
-
-def autodetect_threadcount ():
- try:
- count = multiprocessing.cpu_count()
- except NotImplementedError:
- count = 1
- return count
-
-if __name__ == '__main__':
- # parse command line into arguments and options
- (options, args) = optParser.parse_args()
-
- if len(args) != 1:
- fatalError("\nCall me with the SVG as a parameter.\n\n")
- originalFilename = args[0]
-
- svgFilename = originalFilename + '.svg'
- hotsvgFilename = originalFilename + '.hotspots.svg'
- modes = ['slices']
- if options.remove_shadows:
- modes.append ('shadows')
- if options.invert:
- modes.append ('invert')
-
- with open (svgFilename, 'wb') as output:
- filter_svg(originalFilename, output, modes)
-
- if options.hotspots:
- with open (hotsvgFilename, 'wb') as output:
- filter_svg(originalFilename, output, ['hotspots'])
- # setup program variables from command line (in other words, handle non-option args)
- basename = os.path.splitext(svgFilename)[0]
-
- if options.sliceprefix:
- sliceprefix = options.sliceprefix
- else:
- sliceprefix = ''
-
- if not options.scales:
- del scale_pairs[:]
-
- if options.number_of_renderers <= 0:
- options.number_of_renderers = autodetect_threadcount ()
-
- inkscape_instances = []
-
- for i in range (0, options.number_of_renderers):
- inkscape = subprocess.Popen (['inkscape', '--without-gui', '--shell'], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
- if inkscape is None:
- fatalError("Failed to start Inkscape shell process")
- inkscape_stderr = inkscape.stderr
- inkscape_stderr_thread = Thread (target = stderr_reader, args=(inkscape, inkscape_stderr))
- inkscape_stdin_buf = []
- inkscape_instances.append ([inkscape, inkscape_stderr, inkscape_stderr_thread, inkscape_stdin_buf])
-
- # initialise results before actually attempting to parse the SVG file
- svgBounds = SVGRect(0,0,0,0)
- rectList = []
-
- # Try to parse the svg file
- xmlParser = make_parser()
- xmlParser.setFeature(feature_namespaces, 0)
-
- # setup XML Parser with an SVGLayerHandler class as a callback parser ####
- svgLayerHandler = SVGLayerHandler()
- xmlParser.setContentHandler(svgLayerHandler)
- try:
- xmlParser.parse(svgFilename)
- except SAXParseException, e:
- fatalError("Error parsing SVG file '%s': line %d,col %d: %s. If you're seeing this within inkscape, it probably indicates a bug that should be reported." % (svgFilename, e.getLineNumber(), e.getColumnNumber(), e.getMessage()))
-
- # verify that the svg file actually contained some rectangles.
- if len(svgLayerHandler.svg_rects) == 0:
- fatalError("""No slices were found in this SVG file. Please refer to the documentation for guidance on how to use this SVGSlice. As a quick summary:
-
-""" + usageMsg)
- else:
- dbg("Parsing successful.")
-
- #svgLayerHandler.generateXHTMLPage()
- del xmlParser
-
- skipped = {}
- roundrobin = [0]
-
- # loop through each slice rectangle, and render a PNG image for it
- svgLayerHandler.svg_rects
- for rect in svgLayerHandler.svg_rects:
- slicename = sliceprefix + rect.name
- rect.renderFromSVG(svgFilename, slicename, skipped, roundrobin, hotsvgFilename)
-
- cleanup()
-
- for rect in svgLayerHandler.svg_rects:
- slicename = sliceprefix + rect.name
- postprocess_slice(slicename, skipped)
- if options.hotspots:
- write_xcur(slicename)
-
- if options.hotspots:
- passed = {}
- for rect in svgLayerHandler.svg_rects:
- slicename = sliceprefix + rect.name
- sort_xcur(slicename, passed)
- #if not option.testing:
- # delete_hotspot(slicename)
-
- dbg('Slicing complete.')
+ """Parses an SVG file, extracing slicing rectangles from a "slices" layer"""
+
+ def __init__(self):
+ SVGHandler.__init__(self)
+ self.svg_rects = []
+ self.layer_nests = 0
+
+ def inSlicesLayer(self):
+ return self.layer_nests >= 1
+
+ def add(self, rect):
+ """Adds the given rect to the list of rectangles successfully parsed"""
+ self.svg_rects.append(rect)
+
+ def startElement_layer(self, name, attrs):
+ """Callback hook for parsing layer elements
+
+ Checks to see if we're starting to parse a slices layer, and sets the appropriate flags. Otherwise, the layer will simply be ignored."""
+ id = attrs["id"]
+ debug(f'found layer: name="{name}" id="{id}"')
+ if attrs.get("inkscape:groupmode", None) == "layer":
+ if self.inSlicesLayer() or attrs["inkscape:label"] == "slices":
+ self.layer_nests += 1
+
+ def endElement_layer(self, name):
+ """Callback for leaving a layer in the SVG file
+
+ Just undoes any flags set previously."""
+ debug(f'leaving layer: name="{name}"')
+ if self.inSlicesLayer():
+ self.layer_nests -= 1
+
+ def startElement_rect(self, name, attrs):
+ """Callback for parsing an SVG rectangle
+
+ Checks if we're currently in a special "slices" layer using flags set by startElement_layer(). If we are, the current rectangle is considered to be a slice, and is added to the list of parsed
+ rectangles. Otherwise, it will be ignored."""
+
+ if self.inSlicesLayer():
+ x1 = self.parseCoordinates(attrs["x"])
+ y1 = self.parseCoordinates(attrs["y"])
+ x2 = self.parseCoordinates(attrs["width"]) + x1
+ y2 = self.parseCoordinates(attrs["height"]) + y1
+ name = attrs["id"]
+ rect = SVGRect(x1, y1, x2, y2, name)
+ self.add(rect)
+
+ def startElement(self, name, attrs):
+ """Generic hook for examining and/or parsing all SVG tags"""
+
+ debug(f'Beginning element "{name}"')
+ if name == "svg":
+ self.startElement_svg(name, attrs)
+ elif name == "g":
+ # inkscape layers are groups, I guess, hence 'g'
+ self.startElement_layer(name, attrs)
+ elif name == "rect":
+ self.startElement_rect(name, attrs)
+
+ def endElement(self, name):
+ """Generic hook called when the parser is leaving each SVG tag"""
+ debug('Ending element "%s"' % name)
+ if name == "g":
+ self.endElement_layer(name)
+
+ def generateXHTMLPage(self):
+ """Generates an XHTML page for the SVG rectangles previously parsed."""
+ write = sys.stdout.write
+ write('<?xml version="1.0" encoding="UTF-8"?>\n')
+ write(
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">\n'
+ )
+ write('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">\n')
+ write(" <head>\n")
+ write(" <title>Sample SVGSlice Output</title>\n")
+ write(" </head>\n")
+ write(" <body>\n")
+ write(
+ " <p>Sorry, SVGSlice's XHTML output is currently very basic. Hopefully, it will serve as a quick way to preview all generated slices in your browser, and perhaps as a starting point for further layout work. Feel free to write it and submit a patch to the author :)</p>\n"
+ )
+
+ write(" <p>")
+ for rect in self.svg_rects:
+ write(
+ ' <img src="%s" alt="%s (please add real alternative text for this image)" longdesc="Please add a full description of this image" />\n'
+ % (sliceprefix + rect.name + ".png", rect.name)
+ )
+ write(" </p>")
+
+ write(
+ '<p><a href="http://validator.w3.org/check?uri=referer"><img src="http://www.w3.org/Icons/valid-xhtml10" alt="Valid XHTML 1.0!" height="31" width="88" /></a></p>'
+ )
+
+ write(" </body>\n")
+ write("</html>\n")
+
+
+class SVGFilter(saxutils.XMLFilterBase):
+ def __init__(self, upstream, downstream, mode, **kwargs):
+ saxutils.XMLFilterBase.__init__(self, upstream)
+ self._downstream = downstream
+ self.mode = mode
+
+ def startDocument(self):
+ self.in_throwaway_layer_stack = [False]
+
+ def startElement(self, localname, attrs):
+ def modify_style(style, old_style, new_style=None):
+ styles = style.split(";")
+ new_styles = []
+ if old_style is not None:
+ match_to = old_style + ":"
+ for s in styles:
+ if len(s) > 0 and (old_style is None or not s.startswith(match_to)):
+ new_styles.append(s)
+ if new_style is not None:
+ new_styles.append(new_style)
+ return ";".join(new_styles)
+
+ dict = {}
+ is_throwaway_layer = False
+ is_slices = False
+ is_hotspots = False
+ is_shadows = False
+ is_layer = False
+ if localname == "g":
+ for key, value in attrs.items():
+ if key == "inkscape:label":
+ if value == "slices":
+ is_slices = True
+ elif value == "hotspots":
+ is_hotspots = True
+ elif value == "shadows":
+ is_shadows = True
+ elif key == "inkscape:groupmode":
+ if value == "layer":
+ is_layer = True
+ if MODE_SHADOWS in self.mode and is_shadows:
+ # Only remove the shadows
+ is_throwaway_layer = True
+ elif MODE_HOTSPOTS in self.mode and not (is_hotspots or is_slices):
+ # Remove all layers but hotspots and slices
+ if localname == "g":
+ is_throwaway_layer = True
+ idict = {}
+ idict.update(attrs)
+ if "style" not in attrs.keys():
+ idict["style"] = ""
+ for key, value in idict.items():
+ alocalname = key
+ if alocalname == "style":
+ had_style = True
+ if alocalname == "style" and is_slices:
+ # Make slices invisible. Do not check the mode, because there is
+ # no circumstances where we *want* to render slices
+ value = modify_style(value, "display", "display:none")
+ if alocalname == "style" and is_hotspots:
+ if MODE_HOTSPOTS in self.mode:
+ # Make hotspots visible in hotspots mode
+ value = modify_style(value, "display", "display:inline")
+ else:
+ # Make hotspots invisible otherwise
+ value = modify_style(value, "display", "display:none")
+ if (
+ alocalname == "style"
+ and MODE_INVERT in self.mode
+ and is_layer
+ and is_shadows
+ ):
+ value = modify_style(value, None, "filter:url(#InvertFilter)")
+ dict[key] = value
+
+ if self.in_throwaway_layer_stack[0] or is_throwaway_layer:
+ self.in_throwaway_layer_stack.insert(0, True)
+ else:
+ self.in_throwaway_layer_stack.insert(0, False)
+ attrs = xmlreader.AttributesImpl(dict)
+ self._downstream.startElement(localname, attrs)
+
+ def characters(self, content):
+ if self.in_throwaway_layer_stack[0]:
+ return
+ self._downstream.characters(content)
+
+ def endElement(self, localname):
+ if self.in_throwaway_layer_stack.pop(0):
+ return
+ self._downstream.endElement(localname)
+
+
+def filter_svg(input, output, mode):
+ """filter_svg(input:file, output:file, mode)
+
+ Parses the SVG input from the input stream.
+ For mode == 'hotspots' it filters out all
+ layers except for hotspots and slices. Also makes hotspots
+ visible.
+ For mode == 'shadows' it filters out the shadows layer.
+ """
+
+ mode_objs = [[m] for m in mode]
+ if len(mode_objs) == 0:
+ raise ValueError()
+
+ output_gen = saxutils.XMLGenerator(output)
+ parser = make_parser()
+ filter = SVGFilter(parser, output_gen, mode_objs)
+ filter.setFeature(handler.feature_namespaces, False)
+ filter.setErrorHandler(handler.ErrorHandler())
+ # This little I/O dance is here to ensure that SAX parser does not stash away
+ # an open file descriptor for the input file, which would prevent us from unlinking it later
+ with open(input, "rb") as inp:
+ contents = inp.read()
+ contents_io = io.BytesIO(contents)
+ source_object = saxutils.prepare_input_source(contents_io)
+ filter.parse(source_object)
+ del filter
+ del parser
+ del output_gen
+
+
+def autodetect_threadcount():
+ try:
+ count = multiprocessing.cpu_count()
+ except NotImplementedError:
+ count = 1
+ return count
+
+
+def get_modes(options):
+ modes = ["slices"]
+ if options.remove_shadows:
+ modes.append("shadows")
+ if options.invert:
+ modes.append("invert")
+ return modes
+
+
+def parse_svg_file(filename):
+ """Parse the SVG input file"""
+ xml_parser = make_parser()
+ xml_parser.setFeature(feature_namespaces, 0)
+ handler = SVGLayerHandler()
+ xml_parser.setContentHandler(handler)
+ try:
+ info(f"parsing {filename}")
+ xml_parser.parse(filename)
+ except SAXParseException as e:
+ lineno = e.getLineNumber()
+ colno = e.getColumnNumber()
+ msg = e.getMessage()
+ error(
+ f"Error parsing {filename}, line:{lineno}, column:{colno}, message:{msg}.\n"
+ )
+ fatal(
+ "If are seeing this within inkscape, it probably indicates a bug that should be reported."
+ )
+
+ if len(handler.svg_rects) == 0:
+ fatal(
+ """No slices were found in this SVG file.
+Please refer to the documentation for guidance on how to use this SVGSlice.
+As a quick summary:
+"""
+ + usageMsg
+ )
+ else:
+ debug("Parsing successful.\n")
+
+ # TODO why explicit delete?
+ del xml_parser
+ return handler
+
+
+def stderr_reader(inkscape, inkscape_stderr):
+ """Read from a file descriptor
+ Used to read from inkscape process stderr"""
+ while True:
+ line = inkscape_stderr.readline()
+ if line:
+ line = line.rstrip("\n").rstrip("\r")
+ print(f"inkscape STDERR> {line}")
+ fatal(f"inkscape failed to render a slice. Aborting now")
+ else:
+ raise UnexpectedEndOfStream
+
+
+def spawn_inkscape(number_of_renderers, filename):
+ """ Spawn multiple instances of inkscape as for image rendering """
+ info(f"spawning {number_of_renderers} inkscape instances")
+ for i in range(number_of_renderers):
+ proc = subprocess.Popen(
+ ["flatpak", "run", "org.inkscape.Inkscape", "--shell", filename],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ if not proc:
+ fatal("Failed to start Inkscape shell process")
+
+ thread = Thread(target=stderr_reader, args=(proc, proc.stderr))
+ RENDERERS.append([proc, proc.stderr, thread])
+
+
+def render_pngs(svgLayerHandler, sliceprefix):
+ debug("Loop through each slice rectangle, and render a PNG image for it")
+
+ skipped = {}
+ roundrobin = [0]
+
+ for rect in svgLayerHandler.svg_rects:
+ slicename = sliceprefix + rect.name
+ rect.renderFromSVG(
+ SVG_WORKING_COPY, slicename, skipped, roundrobin, SVG_HOTSPOT_WORKING_COPY
+ )
+
+ return skipped
+
+
+def postprocess(svgLayerHandler, prefix, skipped, hotspots):
+ for rect in svgLayerHandler.svg_rects:
+ slicename = prefix + rect.name
+ postprocess_slice(slicename, skipped)
+ if options.hotspots:
+ write_xcur(slicename)
+
+ if options.hotspots:
+ passed = {}
+ for rect in svgLayerHandler.svg_rects:
+ slicename = prefix + rect.name
+ sort_xcur(slicename, passed)
+ # if not option.testing:
+ # delete_hotspot(slicename)
+
+
+if __name__ == "__main__":
+ options = configure()
+
+ with open(SVG_WORKING_COPY, "wb") as output:
+ filter_svg(options.originalFilename, output, options.modes)
+
+ if options.hotspots:
+ with open(SVG_HOTSPOT_WORKING_COPY, "wb") as output:
+ filter_svg(options.originalFilename, output, ["hotspots"])
+
+ try:
+ spawn_inkscape(options.number_of_renderers, SVG_WORKING_COPY)
+ svgLayerHandler = parse_svg_file(SVG_WORKING_COPY)
+ skipped = render_pngs(svgLayerHandler, options.sliceprefix)
+ postprocess(svgLayerHandler, options.sliceprefix, skipped, options.hotspots)
+ debug("Slicing complete.")
+ finally:
+ cleanup()