From 295b24921c5f6ac1070a3cefa7c6f0f69a570938 Mon Sep 17 00:00:00 2001 From: Mark Wooding Date: Fri, 12 Apr 2019 12:32:08 +0100 Subject: [PATCH 1/1] Initial commit. The script basically works, but is only half-documented. --- rubik | 683 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 683 insertions(+) create mode 100755 rubik diff --git a/rubik b/rubik new file mode 100755 index 0000000..e5197bd --- /dev/null +++ b/rubik @@ -0,0 +1,683 @@ +#! /usr/bin/python +### -*- coding: utf-8 -*- +### +### Rubik cube model and rendering +### +### (c) 2018 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### 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 2 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, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +###-------------------------------------------------------------------------- +### External dependencies. + +import math as M; TAU = 2*M.pi +import os as OS +import sys as SYS + +import cairo as CR +import numpy as NP + +###-------------------------------------------------------------------------- +### The main cube model. + +## Names for the faces and corresponding colours. +U, D, L, R, F, B, UNK = 0, 1, 2, 3, 4, 5, 6 +FACE = ['U', 'D', 'L', 'R', 'F', 'B'] + +## Number of faces. +NFACE = 6 + +## Colours of the faces. +COLOUR = ['#', '+', '%', '/', '=', '-', '·'] +RGB = [(1, 0, 0), (1, 0.75, 0), + (1, 1, 0), (1, 1, 1), + (0, 0, 1), (0, 1, 0), + (0.4, 0.4, 0.4)] +CMAP = { 'r': U, 'o': D, 'y': L, 'w': R, 'b': F, 'g': B, '?': UNK } + +## Geometry table. +## +## For 0 <= k < 6 and 0 <= d < 4, GEOM[k][d] holds a triple (k', ix, iy) with +## the following meaning. If you start at face k, move in the direction d +## (up, down, left, or right), and treat the edge that you crossed as the x +## axis of a right-handed coordinate system, increasing clockwise, then you +## are on face k', and (ix, iy) is a unit vector in the coordinate system of +## the state array pointing in the positive x direction. +GEOM = [[(B, -1, 0), (F, -1, 0), (L, -1, 0), (R, -1, 0)], + [(F, +1, 0), (B, +1, 0), (L, +1, 0), (R, +1, 0)], + [(U, 0, -1), (D, 0, -1), (B, 0, +1), (F, 0, -1)], + [(U, 0, +1), (D, 0, +1), (F, 0, +1), (B, 0, -1)], + [(U, +1, 0), (D, -1, 0), (L, 0, +1), (R, 0, -1)], + [(U, -1, 0), (D, +1, 0), (R, 0, +1), (L, 0, -1)]] + +def convert_side_coords(w, orig, face, x, y): + """ + Convert relative coordinates (X, Y) on a side face into raw coordinates. + + Suppose we start on face ORIG and move to the adjacent FACE (one of `U', + `D', `L', `R'), treating the edge we crossed as the x-axis of a right-hand + coordinate system, increasing clockwise; return a triple (K, U, V), such + that m[K][U][V] identifies the slot in the model corresponding to the given + coordinates. The argument W is the side length of the cube. + """ + k, ix, iy = GEOM[orig][face] + jx, jy = -iy, ix + if ix < 0 or jx < 0: ox = w - 1 + else: ox = 0 + if iy < 0 or jy < 0: oy = w - 1 + else: oy = 0 + return k, ox + x*ix + y*jx, oy + x*iy + y*jy + +def slice_orbits(w, face, dp): + """ + Return the effect of a slice turn on the side stickers. + + Returns a list of lists of (K, X, Y) model coordinates describing the cycle + decomposition of the permutation induced on the side stickers of a + clockwise turn of the DPth slice of a size-W cube as viewed from FACE. + """ + oo = [] + for x in xrange(w): + oo.append([convert_side_coords(w, face, k, x, dp) + for k in [U, R, D, L]]) + return oo + +def face_orbits(w, face, inv): + """ + Return the effect of a face turn on the face's stickers. + + Returns a list of lists of (K, X, Y) model coordinates describing the cycle + decomposition of the permutation induced on the face stickers of a + clockwise (or anticlockwise, if INV is true) turn of a FACE of a size-W + cube. + """ + if inv: ta, tb, tc, td, dx, dy = 0, -1, 1, 0, w - 1, 0 + else: ta, tb, tc, td, dx, dy = 0, 1, -1, 0, 0, w - 1 + oo = [] + for x in xrange((w + 1)//2, w): + for y in xrange(w//2, x + 1): + o = [] + u, v = x, y + for i in xrange(4): + o.append((face, u, v)) + u, v = ta*u + tb*v + dx, tc*u + td*v + dy + oo.append(o) + return oo + +def orbits(w, face, dp): + """ + Return the effect of a slice turn on the cube's stickers. + + Returns a list of lists of (K, X, Y) model coordinates describing the cycle + decompositon of the permutation induced on the cube's stickers of a + clockwise turn of the DPth slice of a size-W cube, as viewed from FACE. + """ + oo = slice_orbits(w, face, dp) + if dp == 0: oo += face_orbits(w, face, False) + elif dp == w - 1: oo += face_orbits(w, face ^ 1, True) + return oo + +def maybe_parens(parens, s): + """If S, then return the string `(S)'; otherwise just return S.""" + if parens: return '(' + s + ')' + else: return s + +class BaseMove (object): + """ + Base class for move objects. + + Subclasses must implement the following methods. + + * apply(PUZ): apply the move to a puzzle PUZ + * invert(): return the inverse of the move + * _str(lv): return a string representation, in a priority-LV context + """ + def repeat(me, m): + """Return the move repeated M times.""" + if m == 0: return NULLMOVE + else: return RepeatedMove(me, m) + def flatten(me): + """Return a list of constituent moves.""" + return [me] + def __str__(me): + """Return a string representation of the move.""" + return me._str(9) + +class NullMove (BaseMove): + """The null move, which does nothing at all.""" + def apply(me, puz): + """Apply the move to the puzzle PUZ.""" + pass + def invert(me): + """Return the inverse of the move.""" + return me + def repeat(me, m): + """Return the move repeated M times.""" + return me + def flatten(me): + """Return a list of constituent moves.""" + return [] + def _str(me, lv): + """Return a string representation, in a priority-LV context.""" + return '#' +NULLMOVE = NullMove() + +class AtomicMove (BaseMove): + """A turn of a single slice.""" + + MAP = { } + for k in xrange(NFACE): + MAP[FACE[k]] = (k, 0) + MAP[FACE[k].lower()] = (k, 1) + + def __init__(me, face, dp = 0, n = 1): + """ + Construct an atomic move. + + Returns a move representing N clockwise turns of the DPth slice, as + viewed from FACE. + """ + me.face = face + me.dp = dp + me.n = n + def apply(me, puz): + """Apply the move to the puzzle PUZ.""" + if me.dp >= 0: puz.turn(me.face, me.dp, me.n) + else: puz.turn_multi(me.face, -me.dp, me.n) + def invert(me): + """Return the inverse of the move.""" + return AtomicMove(me.face, me.dp, -me.n) + def repeat(me, m): + """Return the move repeated M times.""" + mm = (me.n*m)%4 + if not mm: return NULLMOVE + else: return AtomicMove(me.face, me.dp, mm) + def _str(me, lv): + """Return a string representation, in a priority-LV context.""" + f = FACE[me.face] + if me.n%4 == 0: d = '0' + elif me.n%4 == 1: d = '' + elif me.n%4 == 2: d = '2' + elif me.n%4 == 3: d = "'" + if me.dp == 0: return f + d + elif me.dp == 1: return f.lower() + d + elif me.dp < 0: return '%s%s_%d' % (f, d, -me.dp) + +class SpinMove (BaseMove): + """A spin of the entire cube.""" + MAP = { 'X': R, 'Y': U, 'Z': F } + UNMAP = { } + for k, v in MAP.iteritems(): UNMAP[v] = k + + def __init__(me, face, n = 1): + """Construct a spin move. + + Returns a move representing N clockwise spins the entire cube DPth slice, + as viewed from FACE. + """ + me.face = face + me.n = n + def apply(me, puz): + """Apply the move to the puzzle PUZ.""" + puz.turn_multi(me.face, puz.w, me.n) + def invert(me): + """Return the inverse of the move.""" + return SpinMove(me.face, -me.n) + def repeat(me, m): + """Return the move repeated M times.""" + mm = (me.n*m)%4 + if not mm: return NULLMOVE + else: return SpinMove(me.face, mm) + def _str(me, lv): + """Return a string representation, in a priority-LV context.""" + f = me.MAP[me.face] + if me.n%4 == 0: d = '0' + elif me.n%4 == 1: d = '' + elif me.n%4 == 2: d = '2' + elif me.n%4 == 3: d = "'" + return f + d + +class MoveSequence (BaseMove): + """A sequence of moves, applied in left-to-right order.""" + def __init__(me, moves): + """Construct a sequence of moves from the list MOVES.""" + me.moves = reduce(lambda x, y: x + y, [mv.flatten() for mv in moves]) + def apply(me, puz): + """Apply the move to the puzzle PUZ.""" + for mv in me.moves: mv.apply(puz) + def invert(me): + """Return the inverse of the move.""" + return MoveSequence(list(reversed([mv.invert() for mv in me.moves]))) + def _str(me, lv): + """Return a string representation, in a priority-LV context.""" + return maybe_parens(lv < 5, ' '.join([mv._str(5) for mv in me.moves])) + +class RepeatedMove (BaseMove): + """A move applied multiple times.""" + def __init__(me, move, m): + """Construct the move which consists of MOVE repeated M times.""" + me.move = move + me.m = m + def apply(me, puz): + """Apply the move to the puzzle PUZ.""" + for i in xrange(me.m): me.move.apply(puz) + def invert(me): + """Return the inverse of the move.""" + return RepeatedMove(me.move.invert(), me.m) + def repeat(me, m): + """Return the move repeated M times.""" + if m == 0: return NULLMOVE + else: return RepeatedMove(me.move, m*me.m) + def _str(me, lv): + """Return a string representation, in a priority-LV context.""" + return maybe_parens(lv < 3, '%s%d' % (me.move._str(2), me.m)) + +def skip_spaces(s, i): + """Return the smallest index J >= I such that S[J] is not whitespace.""" + while i < len(s) and s[i].isspace(): i += 1 + return i + +def parse_number(s, i): + """ + Parse an integer from S, starting at index I. + + The substring S[I:] consists of zero or more digits, optionally followed by + a nondigit character and any other characters. Convert the digit sequence + into an integer N and return the pair (J, N), where J is the index of the + first non-whitespace character following the digit sequence, or the length + of S if there is no such character. + """ + n = 0 + while i < len(s) and s[i].isdigit(): + n = 10*n + ord(s[i]) - ord('0') + i += 1 + i = skip_spaces(s, i) + return i, n + +def parse_parens(s, i, delim): + i = skip_spaces(s, i) + i, mv = parse_conjugate(s, i) + if s[i] != delim: raise SyntaxError('missing %s' % delim) + i = skip_spaces(s, i + 1) + return i, mv + +def parse_primary(s, i): + if i >= len(s): return i, None + elif s[i] == '(': return parse_parens(s, i + 1, ')') + elif s[i] == '[': return parse_parens(s, i + 1, ']') + elif s[i] in SpinMove.MAP: + k = SpinMove.MAP[s[i]] + i = skip_spaces(s, i + 1) + return i, SpinMove(k, 1) + elif s[i] in AtomicMove.MAP: + k, dp = AtomicMove.MAP[s[i]] + if dp == 0 and i + 2 < len(s) and s[i + 1] == '_' and s[i + 2].isdigit(): + i, n = parse_number(s, i + 2) + if n == 1: dp = 0 + else: dp = -n + else: + i = skip_spaces(s, i + 1) + mv = AtomicMove(k, dp, 1) + return i, mv + else: return i, None + +def parse_move(s, i): + i, mv = parse_primary(s, i) + if mv is None: return i, None + while True: + if i < len(s) and s[i] == "'": + i = skip_spaces(s, i + 1) + mv = mv.invert() + elif i < len(s) and s[i].isdigit(): + i, m = parse_number(s, i) + mv = mv.repeat(m) + elif i + 1 < len(s) and s[i] == '^' and s[i + 1].isdigit(): + i, m = parse_number(s, i + 1) + mv = mv.repeat(m) + else: + break + return i, mv + +def parse_sequence(s, i): + seq = [] + while True: + i, mv = parse_move(s, i) + if mv is None: break + seq.append(mv) + if len(seq) == 0: raise SyntaxError('unrecognized move') + elif len(seq) == 1: return i, seq[0] + else: return i, MoveSequence(seq) + +def parse_commutator(s, i): + i, mv = parse_sequence(s, i) + if i < len(s) and s[i] == ';': + i = skip_spaces(s, i + 1) + i, mv1 = parse_sequence(s, i) + mv = MoveSequence([mv, mv1, mv.invert(), mv1.invert()]) + return i, mv + +def parse_conjugate(s, i): + i, mv = parse_commutator(s, i) + if i < len(s) and (s[i] == '.' or s[i] == '*'): + i = skip_spaces(s, i + 1) + i, mv1 = parse_commutator(s, i) + mv = MoveSequence([mv, mv1, mv.invert()]) + return i, mv + +def parse_moves(s, i = 0): + i = skip_spaces(s, i) + i, mv = parse_conjugate(s, i) + i = skip_spaces(s, i) + if i < len(s): raise SyntaxError('junk at eol') + return mv + +def xltmatrix(dx, dy, dz): + return NP.matrix([[1, 0, 0, dx], + [0, 1, 0, dy], + [0, 0, 1, dz], + [0, 0, 0, 1]], NP.float64) + +def rotmatrix(theta, axis): + R = NP.matrix(NP.identity(4, NP.float64)) + i, j = 0, 1 + if i >= axis: i += 1 + if j >= axis: j += 1 + c, s = M.cos(theta), M.sin(theta) + R[i, i] = R[j, j] = c + R[i, j] = s; R[j, i] = -s + return R + +def scalematrix(sx, sy, sz): + return NP.matrix([[sx, 0, 0, 0], + [0, sy, 0, 0], + [0, 0, sz, 0], + [0, 0, 0, 1]], NP.float64) + +class Transform (object): + def __init__(me, theta_x, theta_y, delta_z): + me.theta_x = theta_x + me.theta_y = theta_y + me.delta_z = delta_z + me._T = xltmatrix(0, 0, delta_z)*\ + rotmatrix(theta_x, 0)*rotmatrix(theta_y, 1) + def project(me, x, y, z): + u, v, w, _ = (me._T*NP.array([x, y, z, 1])[:, NP.newaxis]).A1 + return me.delta_z*u/w, me.delta_z*v/w + +def vector(x, y, z): + return NP.array([x, y, z], NP.float64) + +def unit_vector(v): + mag = M.sqrt(NP.dot(v, v)) + return v/mag + +def shorten(p, q, delta): + u = unit_vector(q - p) + return p + delta*u, q - delta*u + +def along(f, p, q): + return p + f*(q - p) + +class BasePainter (object): + def __init__(me, xf, cr): + me._cr = cr + me._xf = xf + dx, _ = me._cr.device_to_user_distance(0.4, 0) + me._cr.set_line_width(dx) + def set_colour(me, r, g, b): + me._cr.set_source_rgb(r, g, b) + return me + def path(me, pts): + firstp = True + for x, y, z in pts: + u, v = me._xf.project(x, y, z) + if firstp: me._cr.move_to(u, v) + else: me._cr.line_to(u, v) + firstp = False + return me + def _rounded_path_corner(me, r, q1, p1, p2): + q2, q3 = shorten(p1, p2, r) + c1, c2 = along(0.7, q1, p1), along(0.7, q2, p1) + x0, y0 = me._xf.project(*c1) + x1, y1 = me._xf.project(*c2) + x2, y2 = me._xf.project(*q2) + me._cr.curve_to(x0, y0, x1, y1, x2, y2) + return p2, q3 + def _rounded_path_step(me, r, q1, p1, p2): + p2, q3 = me._rounded_path_corner(r, q1, p1, p2) + x3, y3 = me._xf.project(*q3) + me._cr.line_to(x3, y3) + return p2, q3 + def rounded_polygon(me, r, pts): + o0 = o1 = None + for x, y, z in pts: + p2 = vector(x, y, z) + if o0 is None: o0 = p2 + elif o1 is None: + o1 = p1 = p2 + q0, q1 = shorten(o0, p1, r) + x0, y0 = me._xf.project(*q1) + me._cr.move_to(x0, y0) + else: p1, q1 = me._rounded_path_step(r, q1, p1, p2) + p1, q1 = me._rounded_path_step(r, q1, p1, o0) + p1, q1 = me._rounded_path_corner(r, q1, p1, o1) + me._cr.close_path() + return me + def polygon(me, pts): + me.path(pts) + me._cr.close_path() + return me + +class Painter (BasePainter): + def stroke(me): + me._cr.stroke() + return me + def fill(me): + me._cr.fill() + return me + +class MeasuringPainter (BasePainter): + def __init__(me, *args, **kw): + super(MeasuringPainter, me).__init__(*args, **kw) + me.x0 = me.y0 = me.x1 = me.y1 = None + def _update_bbox(me, x0, y0, x1, y1): + if me.x0 is None or me.x0 > x0: me.x0 = x0 + if me.y0 is None or me.y0 > y0: me.y0 = y0 + if me.x1 is None or me.x1 < x1: me.x1 = x1 + if me.y1 is None or me.y1 < y1: me.y1 = y1 + def stroke(me): + x0, y0, x1, y1 = me._cr.stroke_extents() + me._cr.new_path() + me._update_bbox(x0, y0, x1, y1) + return me + def fill(me): + x0, y0, x1, y1 = me._cr.fill_extents() + me._cr.new_path() + me._update_bbox(x0, y0, x1, y1) + return me + +class Puzzle (object): + + def __init__(me, w): + me.w = w + me.m = NP.ndarray((NFACE, w, w), NP.uint8) + + for k in xrange(NFACE): + for y in reversed(xrange(w)): + for x in xrange(w): + me.m[k, x, y] = k + + me.orbits = [[orbits(w, k, dp) for dp in xrange(w)] + for k in xrange(NFACE)] + + def turn(me, face, dp, n): + for o in me.orbits[face][dp]: + c = [me.m[k, x, y] for k, x, y in o] + for i in xrange(4): + k, x, y = o[(i + n)%4] + me.m[k, x, y] = c[i] + return me + + def turn_multi(me, face, dp, n): + for i in xrange(dp): me.turn(face, i, n) + + def fill_face(me, s, i, k, ox, oy, ix, iy, jx, jy): + i = skip_spaces(s, i) + if i >= len(s): return i + firstp = True + for y in xrange(me.w): + if firstp: pass + elif i >= len(s) or s[i] != ',': raise SyntaxError('missing ,') + else: i += 1 + for x in xrange(me.w): + if i >= len(s): raise SyntaxError('unexpected eof') + c = CMAP[s[i]] + me.m[k, ox + x*ix + y*jx, oy + x*iy + y*jy] = c + i += 1 + firstp = False + if i < len(s) and not s[i].isspace(): + raise SyntaxError('face spec too long') + return i + + def fill(me, s): + i = 0 + me.m[:, :, :] = UNK + i = me.fill_face(s, i, U, 0, 2, +1, 0, 0, -1) + i = me.fill_face(s, i, F, 0, 2, +1, 0, 0, -1) + i = me.fill_face(s, i, R, 0, 2, +1, 0, 0, -1) + i = me.fill_face(s, i, B, 2, 2, -1, 0, 0, -1) + i = me.fill_face(s, i, L, 2, 2, -1, 0, 0, -1) + i = me.fill_face(s, i, D, 0, 0, +1, 0, 0, +1) + i = skip_spaces(s, i) + if i < len(s): raise SyntaxError('trailing junk') + return me + + def show(me): + for y in reversed(xrange(me.w)): + print (2*me.w + 2)*' ' + \ + ' '.join([COLOUR[me.m[U, x, y]] + for x in xrange(me.w)]) + print + for y in reversed(xrange(me.w)): + print ' '.join([' '.join([COLOUR[me.m[k, x, y]] + for x in xrange(me.w)]) + for k in [L, F, R, B]]) + print + for y in reversed(xrange(me.w)): + print (2*me.w + 2)*' ' + \ + ' '.join([COLOUR[me.m[D, x, y]] + for x in xrange(me.w)]) + print + + def render_face(me, paint, k, ox, oy, oz, ix, iy, iz, jx, jy, jz): + w = 2.0/me.w + g = w/16.0 + for x in xrange(me.w): + for y in xrange(me.w): + paint.set_colour(*RGB[me.m[k, x, y]]) + paint.rounded_polygon(0.04, + [(ox + (x*w + g)*ix + (y*w + g)*jx, + oy + (x*w + g)*iy + (y*w + g)*jy, + oz + (x*w + g)*iz + (y*w + g)*jz), + (ox + ((x + 1)*w - g)*ix + (y*w + g)*jx, + oy + ((x + 1)*w - g)*iy + (y*w + g)*jy, + oz + ((x + 1)*w - g)*iz + (y*w + g)*jz), + (ox + ((x + 1)*w - g)*ix + ((y + 1)*w - g)*jx, + oy + ((x + 1)*w - g)*iy + ((y + 1)*w - g)*jy, + oz + ((x + 1)*w - g)*iz + ((y + 1)*w - g)*jz), + (ox + (x*w + g)*ix + ((y + 1)*w - g)*jx, + oy + (x*w + g)*iy + ((y + 1)*w - g)*jy, + oz + (x*w + g)*iz + ((y + 1)*w - g)*jz)]).fill() + + def render(me, paint, reflectp): + + if reflectp: + paint.set_colour(0, 0, 0) + paint.rounded_polygon(0.04, [(-1, +1, +4), (+1, +1, +4), + (+1, -1, +4), (-1, -1, +4)]).fill() + paint.rounded_polygon(0.04, [(-1, -4, +1), (+1, -4, +1), + (+1, -4, -1), (-1, -4, -1)]).fill() + paint.rounded_polygon(0.04, [(-4, -1, +1), (-4, +1, +1), + (-4, +1, -1), (-4, -1, -1)]).fill() + me.render_face(paint, D, -1, -4, +1, +1, 0, 0, 0, 0, -1) + me.render_face(paint, B, +1, -1, +4, -1, 0, 0, 0, +1, 0) + me.render_face(paint, L, -4, -1, +1, 0, 0, -1, 0, +1, 0) + + paint.set_colour(0, 0, 0) + paint.rounded_polygon(0.04, [(-1, +1, -1), (-1, +1, +1), + (+1, +1, +1), (+1, -1, +1), + (+1, -1, -1), (-1, -1, -1)]).fill() + + ##paint.set_colour(0.3, 0.3, 0.3) + ##paint.path([(-1, +1, -1), (+1, +1, -1), (+1, +1, +1)]).stroke() + ##paint.path([(+1, +1, -1), (+1, -1, -1)]).stroke() + + me.render_face(paint, U, -1, +1, -1, +1, 0, 0, 0, 0, +1) + me.render_face(paint, F, -1, -1, -1, +1, 0, 0, 0, +1, 0) + me.render_face(paint, R, +1, -1, -1, 0, 0, +1, 0, +1, 0) + +st = Puzzle(3) +invertp = False +reflectp = True +scale = 32 +for a in SYS.argv[1:]: + if not a: pass + elif a[0] == '-' or a[0] == '+': + w = a[1:] + sense = a[0] == '-' + if w.isdigit(): + n = int(w) + if sense: st = Puzzle(n) + else: scale = n + elif w == 'invert': invertp = sense + elif w == 'reflect': reflectp = sense + else: raise SyntaxError('unknown option') + elif a[0] == '=': st.fill(a[1:]) + elif a[0] == ':': parse_moves(a[1:]).apply(st) + elif a == '?': st.show() + else: + p = a + _, e = OS.path.splitext(p) + surf = CR.ImageSurface(CR.FORMAT_ARGB32, 100, 100) + xf = Transform(TAU/10, TAU/10, 8) + cr = CR.Context(surf) + if invertp: cr.scale(scale, scale) + else: cr.scale(scale, -scale) + m = MeasuringPainter(xf, cr) + st.render(m, reflectp) + x0, y0 = cr.user_to_device(m.x0, m.y0) + x1, y1 = cr.user_to_device(m.x1, m.y1) + if x0 > x1: x0, x1 = x1, x0 + if y0 > y1: y0, y1 = y1, y0 + w, h = map(lambda x: int(M.ceil(x)), [x1 - x0, y1 - y0]) + if e == '.pdf': surf = CR.PDFSurface(p, w + 2, h + 2) + elif e == '.png': surf = CR.ImageSurface(CR.FORMAT_ARGB32, w + 2, h + 2) + elif e == '.ps': + surf = CR.PSSurface(p, w + 2, h + 2) + surf.set_eps(True) + else: raise SyntaxError('unrecognized extension') + cr = CR.Context(surf) + cr.translate(1 - x0, 1 - y0) + if invertp: cr.scale(scale, scale) + else: cr.scale(scale, -scale) + paint = Painter(xf, cr) + st.render(paint, reflectp) + surf.show_page() + if e == '.png': surf.write_to_png(p) + surf.finish() + del cr, surf -- 2.11.0