from typing import Iterable import enum import cairo class State(enum.Enum): NONE = 0 COMMAND = 1 INT = 2 FLOAT = 3 class ParseError(Exception): pass class PathCommand(): __slots__ = 'command', 'args', 'arg', def __init__(self): self.command = None self.args = list() self.arg = '' def set_command(self, command: str): self.command = command def arg_addch(self, ch: str): self.arg += ch def arg_next(self, ch: str): if len(self.arg) > 0: self.args.append(self.arg) self.arg = ch def arg_finish(self): if len(self.arg) > 0: self.args.append(self.arg) self.arg = '' def finish(self): if len(self.arg) > 0: self.args.append(self.arg) args = list() for arg in self.args: try: arg.index('.') args.append(float(arg)) except ValueError: args.append(int(arg)) ret = [self.command, args] self.command = None self.args = list() self.arg = '' return ret class Path(): __slots__ = 'commands', def __init__(self, commands: Iterable): self.commands = commands @staticmethod def is_command(c: int): return (c >= ord('a') and c <= ord('z')) or (c >= ord('A') and c <= ord('Z')) @staticmethod def is_number(c: int): return (c >= ord('0') and c <= ord('9')) @staticmethod def parse(text: str): commands = list() command = PathCommand() state = State.NONE for ch in text: if ch == '\r' or ch == '\n' or ch == '\t': continue c = ord(ch) if state is State.NONE: if Path.is_command(c): command.set_command(ch) state = State.COMMAND elif ch == ' ': continue else: raise ParseError() elif state is State.COMMAND: if Path.is_command(c): commands.append(command.finish()) command.set_command(ch) elif Path.is_number(c) or ch == '-': state = State.INT command.arg_addch(ch) elif ch == '.': state = State.FLOAT command.arg_addch(ch) elif ch == ' ': continue else: raise ParseError() elif state is State.INT: if Path.is_command(c): commands.append(command.finish()) command.set_command(ch) elif Path.is_number(c): command.arg_addch(ch) elif ch == '.': command.arg_addch(ch) state = State.FLOAT elif ch == '-': command.arg_next(ch) elif ch == ' ' or ch == ',': command.arg_finish() else: raise ParseError() elif state is State.FLOAT: if Path.is_command(c): commands.append(command.finish()) command.set_command(ch) elif Path.is_number(c): command.arg_addch(ch) elif ch == '-': command.arg_next(ch) elif ch == ' ' or ch == ',': state = State.INT command.arg_finish() else: raise ParseError(ch) if command.command is not None: commands.append(command.finish()) return Path(commands) def horiz_to(self, cr: cairo.Context, x2: float): _, y = cr.get_current_point() return cr.line_to(x2, y) def rel_horiz_to(self, cr: cairo.Context, x2: float): x, y = cr.get_current_point() return cr.line_to(x + x2, y) def vert_to(self, cr: cairo.Context, y2: float): x, _ = cr.get_current_point() return cr.line_to(x, y2) def rel_vert_to(self, cr: cairo.Context, y2: float): x, y = cr.get_current_point() return cr.line_to(x, y + y2) def quad_to_cube(self, cr: cairo.Context, x1: float, y1: float, x2: float, y2: float): x0, y0 = cr.get_current_point() return (2 / 3 * x1 + 1 / 3 * x0, 2 / 3 * y1 + 1 / 3 * y0, 2 / 3 * x1 + 1 / 3 * x2, 2 / 3 * y1 + 1 / 3 * y2) def draw(self, cr: cairo.Context): last = ['X', []] for item in self.commands: command, args = item if command == 'M': for i in range(0, len(args), 2): if i == 0: cr.move_to(*args[i:i+2]) else: cr.line_to(*args[i:i+2]) elif command == 'm': for i in range(0, len(args), 2): if i == 0: cr.rel_move_to(*args[i:i+2]) else: cr.rel_line_to(*args[i:i+2]) elif command == 'L': for i in range(0, len(args), 2): cr.line_to(*args[i:i+2]) elif command == 'l': for i in range(0, len(args), 2): cr.rel_line_to(*args[i:i+2]) elif command == 'H': for arg in args: self.horiz_to(cr, arg) elif command == 'h': for arg in args: self.rel_horiz_to(cr, arg) elif command == 'V': for arg in args: self.vert_to(cr, arg) elif command == 'v': for arg in args: self.rel_vert_to(cr, arg) elif command == 'C': for i in range(0, len(args), 6): cr.curve_to(*args[i:i+6]) elif command == 'c': for i in range(0, len(args), 6): cr.rel_curve_to(*args[i:i+6]) elif command == 'S' or command == 's': for i in range(0, len(args), 4): x2, y2, x, y = map(float, args[i:i+4]) if last[0] == 'C' or last[0] == 'c': x1, y1 = map(float, last[1][2:4]) elif last[0] == 'S' or last[0] == 's': raise NotImplementedError else: x1, y1 = cr.get_current_point() if command == 'S': cr.curve_to(x1, y1, x2, y2, x, y) elif command == 's': cr.curve_to(x1, y1, x2, y2, x, y) elif command == 'Q' or command == 'q': for i in range(0, len(args), 4): x1, y1, x2, y2 = args[i:i+4] cube = self.quad_to_cube(cr, x1, y1, x2, y2) if command == 'Q': cr.curve_to(*cube) elif command == 'q': cr.rel_curve_to(*cube) elif command == 'T' or command == 't': for i in range(0, len(args), 2): x, y = args[i:i+2] if last[0] == 'Q' or last[0] == 'q': x1, y1 = last[1][0:2] elif last[0] == 'T' or last[0] == 't': raise NotImplementedError elif command == 'Z' or command == 'z': cr.close_path() last = item