1#!/usr/local/bin/python3.8
2
3###########################################################################
4#
5# xasy2asy provides a Python interface to Asymptote
6#
7#
8# Authors: Orest Shardt, Supakorn Rassameemasmuang, and John C. Bowman
9#
10###########################################################################
11
12import PyQt5.QtWidgets as Qw
13import PyQt5.QtGui as Qg
14import PyQt5.QtCore as Qc
15import PyQt5.QtSvg as Qs
16
17import numpy as np
18
19import sys
20import os
21import signal
22import threading
23import string
24import subprocess
25import tempfile
26import re
27import shutil
28import copy
29import queue
30import io
31import atexit
32import DebugFlags
33
34import xasyUtils as xu
35import xasyArgs as xa
36import xasyOptions as xo
37import xasySvg as xs
38
39class AsymptoteEngine:
40    xasy=chr(4)+"\n"
41
42    def __init__(self, path=None, keepFiles=DebugFlags.keepFiles, keepDefaultArgs=True):
43        if path is None:
44            path = xa.getArgs().asypath
45            if path is None:
46                opt = xo.BasicConfigs.defaultOpt
47                opt.load()
48                path = opt['asyPath']
49
50        if sys.platform[:3] == 'win':
51            rx = 0  # stdin
52            wa = 2  # stderr
53        else:
54            rx, wx = os.pipe()
55            ra, wa = os.pipe()
56            os.set_inheritable(rx, True)
57            os.set_inheritable(wx, True)
58            os.set_inheritable(ra, True)
59            os.set_inheritable(wa, True)
60            self.ostream = os.fdopen(wx, 'w')
61            self.istream = os.fdopen(ra, 'r')
62
63        self.keepFiles = keepFiles
64        if sys.platform[:3] == 'win':
65            self.tmpdir = tempfile.mkdtemp(prefix='xasyData_',dir='./')+'/'
66        else:
67            self.tmpdir = tempfile.mkdtemp(prefix='xasyData_')+os.sep
68
69        self.args=['-xasy', '-noV', '-q', '-inpipe=' + str(rx), '-outpipe=' + str(wa), '-o', self.tmpdir]
70
71        self.asyPath = path
72        self.asyProcess = None
73
74    def start(self):
75        try:
76            if sys.platform[:3] == 'win':
77                self.asyProcess = subprocess.Popen([self.asyPath] + self.args,
78                                                stdin=subprocess.PIPE, stderr=subprocess.PIPE,
79                                                universal_newlines=True)
80                self.ostream = self.asyProcess.stdin
81                self.istream = self.asyProcess.stderr
82            else:
83                self.asyProcess = subprocess.Popen([self.asyPath] + self.args,close_fds=False)
84        finally:
85            atexit.register(self.cleanup)
86
87    def wait(self):
88        if self.asyProcess.returncode is not None:
89            return
90        else:
91            return self.asyProcess.wait()
92
93    def __enter__(self):
94        self.start()
95        return self
96
97    def __exit__(self, exc_type, exc_val, exc_tb):
98        self.stop()
99        self.wait()
100
101    @property
102    def tempDirName(self):
103        return self.tmpdir
104
105    def startThenStop(self):
106        self.start()
107        self.stop()
108        self.wait()
109
110    @property
111    def active(self):
112        if self.asyProcess is None:
113            return False
114        return self.asyProcess.returncode is None
115
116    def stop(self):
117        if self.active:
118            self.asyProcess.kill()
119
120    def cleanup(self):
121        self.stop()
122        if self.asyProcess is not None:
123            self.asyProcess.wait()
124        if not self.keepFiles:
125            if os.path.isdir(self.tempDirName + os.sep):
126                shutil.rmtree(self.tempDirName, ignore_errors=True)
127
128class asyTransform(Qc.QObject):
129    """A python implementation of an asy transform"""
130
131    def __init__(self, initTuple, delete=False):
132        """Initialize the transform with a 6 entry tuple"""
133        super().__init__()
134        if isinstance(initTuple, (tuple, list)) and len(initTuple) == 6:
135            self.t = initTuple
136            self.x, self.y, self.xx, self.xy, self.yx, self.yy = initTuple
137            self._deleted = delete
138        else:
139            raise TypeError("Illegal initializer for asyTransform")
140
141    @property
142    def deleted(self):
143        return self._deleted
144
145    @deleted.setter
146    def deleted(self, value):
147        self._deleted = value
148
149    @classmethod
150    def zero(cls):
151        return asyTransform((0, 0, 0, 0, 0, 0))
152
153    @classmethod
154    def fromQTransform(cls, transform: Qg.QTransform):
155        tx, ty = transform.dx(), transform.dy()
156        xx, xy, yx, yy = transform.m11(), transform.m21(), transform.m12(), transform.m22()
157
158        return asyTransform((tx, ty, xx, xy, yx, yy))
159
160    @classmethod
161    def fromNumpyMatrix(cls, transform: np.ndarray):
162        assert transform.shape == (3, 3)
163
164        tx = transform[0, 2]
165        ty = transform[1, 2]
166
167        xx, xy, yx, yy = transform[0:2, 0:2].ravel().tolist()[0]
168
169        return asyTransform((tx, ty, xx, xy, yx, yy))
170
171    def getRawCode(self):
172        return xu.tuple2StrWOspaces(self.t)
173
174    def getCode(self, asy2psmap=None):
175        """Obtain the asy code that represents this transform"""
176        if asy2psmap is None:
177            asy2psmap = asyTransform((0, 0, 1, 0, 0, 1))
178        if self.deleted:
179            return 'zeroTransform'
180        else:
181            return (asy2psmap.inverted() * self * asy2psmap).getRawCode()
182
183    def scale(self, s):
184        return asyTransform((0, 0, s, 0, 0, s)) * self
185
186    def toQTransform(self):
187        return Qg.QTransform(self.xx, self.yx, self.xy, self.yy, self.x, self.y)
188
189    def __str__(self):
190        """Equivalent functionality to getCode(). It allows the expression str(asyTransform) to be meaningful."""
191        return self.getCode()
192
193    def isIdentity(self):
194        return self == identity()
195
196    def inverted(self):
197        return asyTransform.fromQTransform(self.toQTransform().inverted()[0])
198
199    def __eq__(self, other):
200        return list(self.t) == list(other.t)
201
202    def __mul__(self, other):
203        """Define multiplication of transforms as composition."""
204        if isinstance(other, tuple):
205            if len(other) == 6:
206                return self * asyTransform(other)
207            elif len(other) == 2:
208                return ((self.t[0] + self.t[2] * other[0] + self.t[3] * other[1]),
209                        (self.t[1] + self.t[4] * other[0] + self.t[5] * other[1]))
210            else:
211                raise Exception("Illegal multiplier of {:s}".format(str(type(other))))
212        elif isinstance(other, asyTransform):
213            result = asyTransform((0, 0, 0, 0, 0, 0))
214            result.x = self.x + self.xx * other.x + self.xy * other.y
215            result.y = self.y + self.yx * other.x + self.yy * other.y
216            result.xx = self.xx * other.xx + self.xy * other.yx
217            result.xy = self.xx * other.xy + self.xy * other.yy
218            result.yx = self.yx * other.xx + self.yy * other.yx
219            result.yy = self.yx * other.xy + self.yy * other.yy
220            result.t = (result.x, result.y, result.xx, result.xy, result.yx, result.yy)
221            return result
222        elif isinstance(other, str):
223            if other != 'cycle':
224                raise TypeError
225            else:
226                return 'cycle'
227        else:
228            raise TypeError("Illegal multiplier of {:s}".format(str(type(other))))
229
230
231def identity():
232    return asyTransform((0, 0, 1, 0, 0, 1))
233
234def yflip():
235    return asyTransform((0, 0, 1, 0, 0, -1))
236
237class asyObj(Qc.QObject):
238    """A base class for asy objects: an item represented by asymptote code."""
239    def __init__(self):
240        """Initialize the object"""
241        super().__init__()
242        self.asyCode = ''
243
244    def updateCode(self, ps2asymap=identity()):
245        """Update the object's code: should be overriden."""
246        raise NotImplementedError
247
248    def getCode(self, ps2asymap=identity()):
249        """Return the code describing the object"""
250        self.updateCode(ps2asymap)
251        return self.asyCode
252
253
254class asyPen(asyObj):
255    """A python wrapper for an asymptote pen"""
256    @staticmethod
257    def getColorFromQColor(color):
258        return color.redF(), color.greenF(), color.blueF()
259
260    @staticmethod
261    def convertToQColor(color):
262        r, g, b = color
263        return Qg.QColor.fromRgbF(r, g, b)
264
265    @classmethod
266    def fromAsyPen(cls, pen):
267        assert isinstance(pen, cls)
268        return cls(asyengine=pen._asyengine, color=pen.color, width=pen.width, pen_options=pen.options)
269
270    def __init__(self, asyengine=None, color=(0, 0, 0), width=0.5, pen_options=""):
271        """Initialize the pen"""
272        asyObj.__init__(self)
273        self.color = (0, 0, 0)
274        self.options = pen_options
275        self.width = width
276        self._asyengine = asyengine
277        self._deferAsyfy = False
278        if pen_options:
279            self._deferAsyfy = True
280        self.updateCode()
281        self.setColor(color)
282
283    @property
284    def asyEngine(self):
285        return self._asyengine
286
287    @asyEngine.setter
288    def asyEngine(self, value):
289        self._asyengine = value
290
291    def updateCode(self, asy2psmap=identity()):
292        """Generate the pen's code"""
293        if self._deferAsyfy:
294            self.computeColor()
295        self.asyCode = 'rgb({:g},{:g},{:g})+{:s}'.format(self.color[0], self.color[1], self.color[2], str(self.width))
296        if len(self.options) > 0:
297            self.asyCode = self.asyCode + '+' + self.options
298
299    def setWidth(self, newWidth):
300        """Set the pen's width"""
301        self.width = newWidth
302        self.updateCode()
303
304    def setColor(self, color):
305        """Set the pen's color"""
306        if isinstance(color, tuple) and len(color) == 3:
307            self.color = color
308        else:
309            self.color = (0, 0, 0)
310        self.updateCode()
311
312    def setColorFromQColor(self, color):
313        self.setColor(asyPen.getColorFromQColor(color))
314
315    def computeColor(self):
316        """Find out the color of an arbitrary asymptote pen."""
317        assert isinstance(self.asyEngine, AsymptoteEngine)
318        assert self.asyEngine.active
319
320        fout = self.asyEngine.ostream
321        fin = self.asyEngine.istream
322
323        fout.write("pen p=" + self.getCode() + ';\n')
324        fout.write("write(_outpipe,colorspace(p),newl);\n")
325        fout.write("write(_outpipe,colors(p));\n")
326        fout.write("flush(_outpipe);\n")
327        fout.write(self.asyEngine.xasy)
328        fout.flush()
329
330        colorspace = fin.readline()
331        if colorspace.find("cmyk") != -1:
332            lines = fin.readline() + fin.readline() + fin.readline() + fin.readline()
333            parts = lines.split()
334            c, m, y, k = eval(parts[0]), eval(parts[1]), eval(parts[2]), eval(parts[3])
335            k = 1 - k
336            r, g, b = ((1 - c) * k, (1 - m) * k, (1 - y) * k)
337        elif colorspace.find("rgb") != -1:
338            lines = fin.readline() + fin.readline() + fin.readline()
339            parts = lines.split()
340            r, g, b = eval(parts[0]), eval(parts[1]), eval(parts[2])
341        elif colorspace.find("gray") != -1:
342            lines = fin.readline()
343            parts = lines.split()
344            r = g = b = eval(parts[0])
345        else:
346            raise ChildProcessError('Asymptote error.')
347        self.color = (r, g, b)
348        self._deferAsyfy = False
349
350    def tkColor(self):
351        """Return the tk version of the pen's color"""
352        self.computeColor()
353        return '#{}'.format("".join(["{:02x}".format(min(int(256 * a), 255)) for a in self.color]))
354
355    def toQPen(self):
356        if self._deferAsyfy:
357            self.computeColor()
358        newPen = Qg.QPen()
359        newPen.setColor(asyPen.convertToQColor(self.color))
360        newPen.setWidthF(self.width)
361
362        return newPen
363
364
365class asyPath(asyObj):
366    """A python wrapper for an asymptote path"""
367
368    def __init__(self, asyengine: AsymptoteEngine=None, forceCurve=False):
369        """Initialize the path to be an empty path: a path with no nodes, control points, or links."""
370        super().__init__()
371        self.nodeSet = []
372        self.linkSet = []
373        self.forceCurve = forceCurve
374        self.controlSet = []
375        self.computed = False
376        self.asyengine = asyengine
377
378    @classmethod
379    def fromPath(cls, oldPath):
380        newObj = asyPath(None)
381        newObj.nodeSet = copy.copy(oldPath.nodeSet)
382        newObj.linkSet = copy.copy(oldPath.linkSet)
383        newObj.controlSet = copy.deepcopy(oldPath.controlSet)
384        newObj.computed = oldPath.computed
385        newObj.asyengine = oldPath.asyengine
386
387        return newObj
388
389    @classmethod
390    def fromBezierPoints(cls, pointList: list, engine=None):
391        if not pointList:
392            return None
393        assert isinstance(pointList[0], BezierCurveEditor.BezierPoint)
394        nodeList = []
395        controlList = []
396        for point in pointList:
397            nodeList.append(BezierCurveEditor.QPoint2Tuple(point.point))
398            if point.rCtrlPoint is not None:  # first
399                controlList.append([BezierCurveEditor.QPoint2Tuple(point.rCtrlPoint)])
400            if point.lCtrlPoint is not None:  # last
401                controlList[-1].append(BezierCurveEditor.QPoint2Tuple(point.lCtrlPoint))
402        newPath = asyPath(asyengine=engine)
403        newPath.initFromControls(nodeList, controlList)
404        return newPath
405
406    def setInfo(self, path):
407        self.nodeSet = copy.copy(path.nodeSet)
408        self.linkSet = copy.copy(path.linkSet)
409        self.controlSet = copy.deepcopy(path.controlSet)
410        self.computed = path.computed
411
412    @property
413    def isEmpty(self):
414        return len(self.nodeSet) == 0
415
416    @property
417    def isDrawable(self):
418        return len(self.nodeSet) >= 2
419
420    def toQPainterPath(self) -> Qg.QPainterPath:
421        return self.toQPainterPathCurve() if self.containsCurve else self.toQPainterPathLine()
422
423    def toQPainterPathLine(self):
424        baseX, baseY = self.nodeSet[0]
425        painterPath = Qg.QPainterPath(Qc.QPointF(baseX, baseY))
426
427        for pointIndex in range(1, len(self.nodeSet)):
428            node = self.nodeSet[pointIndex]
429            if self.nodeSet[pointIndex] == 'cycle':
430                node = self.nodeSet[0]
431
432            painterPath.lineTo(*node)
433
434        return painterPath
435
436
437    def toQPainterPathCurve(self):
438        if not self.computed:
439            self.computeControls()
440
441        baseX, baseY = self.nodeSet[0]
442        painterPath = Qg.QPainterPath(Qc.QPointF(baseX, baseY))
443
444        for pointIndex in range(1, len(self.nodeSet)):
445            node = self.nodeSet[pointIndex]
446            if self.nodeSet[pointIndex] == 'cycle':
447                node = self.nodeSet[0]
448            endPoint = Qc.QPointF(node[0], node[1])
449            ctrlPoint1 = Qc.QPointF(self.controlSet[pointIndex-1][0][0], self.controlSet[pointIndex-1][0][1])
450            ctrlPoint2 = Qc.QPointF(self.controlSet[pointIndex-1][1][0], self.controlSet[pointIndex-1][1][1])
451
452            painterPath.cubicTo(ctrlPoint1, ctrlPoint2, endPoint)
453        return painterPath
454
455    def initFromNodeList(self, nodeSet, linkSet):
456        """Initialize the path from a set of nodes and link types, "--", "..", or "::" """
457        if len(nodeSet) > 0:
458            self.nodeSet = nodeSet[:]
459            self.linkSet = linkSet[:]
460            self.computed = False
461
462    def initFromControls(self, nodeSet, controlSet):
463        """Initialize the path from nodes and control points"""
464        self.controlSet = controlSet[:]
465        self.nodeSet = nodeSet[:]
466        self.computed = True
467
468    def makeNodeStr(self, node):
469        """Represent a node as a string"""
470        if node == 'cycle':
471            return node
472        else:
473            # if really want to, disable this rounding
474            # shouldn't be to much of a problem since 10e-6 is quite small...
475            return '({:.6g},{:.6g})'.format(node[0], node[1])
476
477    def updateCode(self, ps2asymap=identity()):
478        """Generate the code describing the path"""
479        # currently at postscript. Convert to asy
480        asy2psmap =  ps2asymap.inverted()
481        with io.StringIO() as rawAsyCode:
482            count = 0
483            rawAsyCode.write(self.makeNodeStr(asy2psmap * self.nodeSet[0]))
484            for node in self.nodeSet[1:]:
485                if not self.computed or count >= len(self.controlSet):
486                    rawAsyCode.write(self.linkSet[count])
487                    rawAsyCode.write(self.makeNodeStr(asy2psmap * node))
488                else:
489                    rawAsyCode.write('..controls ')
490                    rawAsyCode.write(self.makeNodeStr(asy2psmap *  self.controlSet[count][0]))
491                    rawAsyCode.write(' and ')
492                    rawAsyCode.write(self.makeNodeStr(asy2psmap * self.controlSet[count][1]))
493                    rawAsyCode.write(".." + self.makeNodeStr(asy2psmap * node))
494                count = count + 1
495            self.asyCode = rawAsyCode.getvalue()
496
497    @property
498    def containsCurve(self):
499        return '..' in self.linkSet or self.forceCurve
500
501    def getNode(self, index):
502        """Return the requested node"""
503        return self.nodeSet[index]
504
505    def getLink(self, index):
506        """Return the requested link"""
507        return self.linkSet[index]
508
509    def setNode(self, index, newNode):
510        """Set a node to a new position"""
511        self.nodeSet[index] = newNode
512
513    def moveNode(self, index, offset):
514        """Translate a node"""
515        if self.nodeSet[index] != "cycle":
516            self.nodeSet[index] = (self.nodeSet[index][0] + offset[0], self.nodeSet[index][1] + offset[1])
517
518    def setLink(self, index, ltype):
519        """Change the specified link"""
520        self.linkSet[index] = ltype
521
522    def addNode(self, point, ltype):
523        """Add a node to the end of a path"""
524        self.nodeSet.append(point)
525        if len(self.nodeSet) != 1:
526            self.linkSet.append(ltype)
527        if self.computed:
528            self.computeControls()
529
530    def insertNode(self, index, point, ltype=".."):
531        """Insert a node, and its corresponding link, at the given index"""
532        self.nodeSet.insert(index, point)
533        self.linkSet.insert(index, ltype)
534        if self.computed:
535            self.computeControls()
536
537    def setControl(self, index, position):
538        """Set a control point to a new position"""
539        self.controlSet[index] = position
540
541    def popNode(self):
542        if len(self.controlSet) == len(self.nodeSet):
543            self.controlSet.pop()
544        self.nodeSet.pop()
545        self.linkSet.pop()
546
547    def moveControl(self, index, offset):
548        """Translate a control point"""
549        self.controlSet[index] = (self.controlSet[index][0] + offset[0], self.controlSet[index][1] + offset[1])
550
551    def computeControls(self):
552        """Evaluate the code of the path to obtain its control points"""
553        # For now, if no asymptote process is given spawns a new one.
554        # Only happens if asyengine is None.
555        if self.asyengine is not None:
556            assert isinstance(self.asyengine, AsymptoteEngine)
557            assert self.asyengine.active
558            asy = self.asyengine
559            startUp = False
560        else:
561            startUp = True
562            asy = AsymptoteEngine()
563            asy.start()
564
565        fout = asy.ostream
566        fin = asy.istream
567
568        fout.write("path p=" + self.getCode() + ';\n')
569        fout.write("write(_outpipe,length(p),newl);\n")
570        fout.write("write(_outpipe,unstraighten(p),endl);\n")
571        fout.write(asy.xasy)
572        fout.flush()
573
574        lengthStr = fin.readline()
575        pathSegments = eval(lengthStr.split()[-1])
576        pathStrLines = []
577        for i in range(pathSegments + 1):
578            line = fin.readline()
579            line = line.replace("\n", "")
580            pathStrLines.append(line)
581        oneLiner = "".join(pathStrLines).replace(" ", "")
582        splitList = oneLiner.split("..")
583        nodes = [a for a in splitList if a.find("controls") == -1]
584        self.nodeSet = []
585        for a in nodes:
586            if a == 'cycle':
587                self.nodeSet.append(a)
588            else:
589                self.nodeSet.append(eval(a))
590        controls = [a.replace("controls", "").split("and") for a in splitList if a.find("controls") != -1]
591        self.controlSet = [[eval(a[0]), eval(a[1])] for a in controls]
592        self.computed = True
593
594        if startUp:
595            asy.stop()
596
597class asyLabel(asyObj):
598    """A python wrapper for an asy label"""
599
600    def __init__(self, text="", location=(0, 0), pen=None, align=None, fontSize:int=None):
601        """Initialize the label with the given test, location, and pen"""
602        asyObj.__init__(self)
603        self.align = align
604        self.pen = pen
605        self.fontSize = fontSize
606        if align is None:
607            self.align = 'SE'
608        if pen is None:
609            self.pen = asyPen()
610        self.text = text
611        self.location = location
612
613    def updateCode(self, asy2psmap=identity()):
614        """Generate the code describing the label"""
615        newLoc = asy2psmap.inverted() * self.location
616        locStr = xu.tuple2StrWOspaces(newLoc)
617        self.asyCode = 'Label("{0}",{1},p={2}{4},align={3})'.format(self.text, locStr, self.pen.getCode(), self.align,
618        self.getFontSizeText())
619
620    def getFontSizeText(self):
621        if self.fontSize is not None:
622            return '+fontsize({:.6g})'.format(self.fontSize)
623        else:
624            return ''
625
626    def setText(self, text):
627        """Set the label's text"""
628        self.text = text
629        self.updateCode()
630
631    def setPen(self, pen):
632        """Set the label's pen"""
633        self.pen = pen
634        self.updateCode()
635
636    def moveTo(self, newl):
637        """Translate the label's location"""
638        self.location = newl
639
640
641class asyImage:
642    """A structure containing an image and its format, bbox, and IDTag"""
643    def __init__(self, image, format, bbox, transfKey=None, keyIndex=0):
644        self.image = image
645        self.format = format
646        self.bbox = bbox
647        self.IDTag = None
648        self.key = transfKey
649        self.keyIndex = keyIndex
650
651class xasyItem(Qc.QObject):
652    """A base class for items in the xasy GUI"""
653    mapString = 'xmap'
654    setKeyFormatStr = string.Template('$map("{:s}",{:s});').substitute(map=mapString)
655    setKeyAloneFormatStr = string.Template('$map("{:s}");').substitute(map=mapString)
656    resizeComment="// Resize to initial xasy transform"
657    asySize=""
658    def __init__(self, canvas=None, asyengine=None):
659        """Initialize the item to an empty item"""
660        super().__init__()
661        self.transfKeymap = {}              # the new keymap.
662        # should be a dictionary to a list...
663        self.asyCode = ''
664        self.imageList = []
665        self.IDTag = None
666        self.asyfied = False
667        self.onCanvas = canvas
668        self.keyBuffer = None
669        self._asyengine = asyengine
670        self.drawObjects = []
671        self.drawObjectsMap = {}
672        self.setKeyed = True
673        self.unsetKeys = set()
674        self.userKeys = set()
675        self.lineOffset = 0
676        self.imageHandleQueue = queue.Queue()
677
678    def updateCode(self, ps2asymap=identity()):
679        """Update the item's code: to be overriden"""
680        with io.StringIO() as rawCode:
681            transfCode = self.getTransformCode()
682            objCode = self.getObjectCode()
683
684            rawCode.write(transfCode)
685            rawCode.write(objCode)
686            self.asyCode = rawCode.getvalue()
687
688        return len(transfCode.splitlines()), len(objCode.splitlines())
689
690    @property
691    def asyengine(self):
692        return self._asyengine
693
694    @asyengine.setter
695    def asyengine(self, value):
696        self._asyengine = value
697
698    def getCode(self, ps2asymap=identity()):
699        """Return the code describing the item"""
700        self.updateCode(ps2asymap)
701        return self.asyCode
702
703    def getTransformCode(self, asy2psmap=identity()):
704        raise NotImplementedError
705
706    def getObjectCode(self, asy2psmap=identity()):
707        raise NotImplementedError
708
709    def generateDrawObjects(self):
710        raise NotImplementedError
711
712    def handleImageReception(self, file, fileformat, bbox, count, key=None, localCount=0, containsClip=False):
713        """Receive an image from an asy deconstruction. It replaces the default n asyProcess."""
714        # image = Image.open(file).transpose(Image.FLIP_TOP_BOTTOM)
715        if fileformat == 'png':
716            image = Qg.QImage(file)
717        elif fileformat == 'svg':
718            if containsClip:
719                image = xs.SvgObject(self.asyengine.tempDirName+file)
720            else:
721                image = Qs.QSvgRenderer(file)
722                assert image.isValid()
723        else:
724            raise Exception('Format not supported!')
725        self.imageList.append(asyImage(image, fileformat, bbox, transfKey=key, keyIndex=localCount))
726        if self.onCanvas is not None:
727            # self.imageList[-1].iqt = ImageTk.PhotoImage(image)
728            currImage = self.imageList[-1]
729            currImage.iqt = image
730            currImage.originalImage = image
731            currImage.originalImage.theta = 0.0
732            currImage.originalImage.bbox = list(bbox)
733            currImage.performCanvasTransform = False
734
735            # handle this case if transform is not in the map yet.
736            # if deleted - set transform to 0, 0, 0, 0, 0
737            transfExists = key in self.transfKeymap.keys()
738            if transfExists:
739                transfExists = localCount <= len(self.transfKeymap[key]) - 1
740                if transfExists:
741                    validKey = not self.transfKeymap[key][localCount].deleted
742            else:
743                validKey = False
744
745            if (not transfExists) or validKey:
746                currImage.IDTag = str(file)
747                newDrawObj = DrawObject(currImage.iqt, self.onCanvas['canvas'], transform=identity(),
748                                        btmRightanchor=Qc.QPointF(bbox[0], bbox[2]), drawOrder=-1, key=key,
749                                        parentObj=self, keyIndex=localCount)
750                newDrawObj.setBoundingBoxPs(bbox)
751                newDrawObj.setParent(self)
752
753                self.drawObjects.append(newDrawObj)
754
755                if key not in self.drawObjectsMap.keys():
756                    self.drawObjectsMap[key] = [newDrawObj]
757                else:
758                    self.drawObjectsMap[key].append(newDrawObj)
759        return containsClip
760    def asyfy(self, force=False):
761        if self.asyengine is None:
762            return 1
763        if self.asyfied and not force:
764            return
765
766        self.drawObjects = []
767        self.drawObjectsMap.clear()
768        assert isinstance(self.asyengine, AsymptoteEngine)
769        self.imageList = []
770
771        self.unsetKeys.clear()
772        self.userKeys.clear()
773
774        self.imageHandleQueue = queue.Queue()
775        worker = threading.Thread(target=self.asyfyThread, args=[])
776        worker.start()
777        item = self.imageHandleQueue.get()
778        cwd=os.getcwd();
779        os.chdir(self.asyengine.tempDirName)
780        while item != (None,) and item[0] != "ERROR":
781            if item[0] == "OUTPUT":
782                print(item[1])
783            else:
784                keepFile = self.handleImageReception(*item)
785                if not DebugFlags.keepFiles and not keepFile:
786                    try:
787                        os.remove(item[0])
788                        pass
789                    except OSError:
790                        pass
791                    finally:
792                        pass
793            item = self.imageHandleQueue.get()
794        # self.imageHandleQueue.task_done()
795        os.chdir(cwd);
796
797        worker.join()
798
799    def asyfyThread(self):
800        """Convert the item to a list of images by deconstructing this item's code"""
801        assert self.asyengine.active
802
803        fout = self.asyengine.ostream
804        fin = self.asyengine.istream
805
806        self.lineOffset = len(self.getTransformCode().splitlines())
807
808        fout.write("reset\n")
809        fout.flush();
810        for line in self.getCode().splitlines():
811            if DebugFlags.printDeconstTranscript:
812                print('fout:', line)
813            fout.write(line+"\n")
814        fout.write(self.asySize)
815        fout.write("deconstruct();\n")
816        fout.write('write(_outpipe,yscale(-1)*currentpicture.calculateTransform(),endl);\n')
817        fout.write(self.asyengine.xasy)
818        fout.flush()
819
820        imageInfos = []                                 # of (box, key)
821        n = 0
822
823        keyCounts = {}
824
825        def render():
826            for i in range(len(imageInfos)):
827                box, key, localCount, useClip = imageInfos[i]
828                l, b, r, t = [float(a) for a in box.split()]
829                name = "_{:d}.{:s}".format(i, fileformat)
830
831                self.imageHandleQueue.put((name, fileformat, (l, -t, r, -b), i, key, localCount, useClip))
832
833        # key first, box second.
834        # if key is "Done"
835        raw_text = fin.readline()
836        text = ""
837        if DebugFlags.printDeconstTranscript:
838            print(raw_text.strip())
839
840        # template=AsyTempDir+"%d_%d.%s"
841        fileformat = 'svg'
842
843        while raw_text != "Done\n" and raw_text != "Error\n":
844#            print(raw_text)
845            text = fin.readline()       # the actual bounding box.
846            # print('TESTING:', text)
847            keydata = raw_text.strip().replace('KEY=', '', 1)  # key
848
849            clipflag = keydata[-1] == '1'
850            userkey = keydata[-2] == '1'
851            keydata = keydata[:-3]
852
853            if not userkey:
854                self.unsetKeys.add(keydata)     # the line and column to replace.
855            else:
856                self.userKeys.add(keydata)
857
858#                print(line, col)
859
860            if keydata not in keyCounts.keys():
861                keyCounts[keydata] = 0
862
863            imageInfos.append((text, keydata, keyCounts[keydata], clipflag))      # key-data pair
864
865            # for the next item
866            keyCounts[keydata] += 1
867
868            raw_text = fin.readline()
869
870            if DebugFlags.printDeconstTranscript:
871                print(text.rstrip())
872                print(raw_text.rstrip())
873
874            n += 1
875
876        if raw_text != "Error\n":
877            if text == "Error\n":
878                self.imageHandleQueue.put(("ERROR", fin.readline()))
879            else:
880                render()
881
882            self.asy2psmap = asyTransform(xu.listize(fin.readline().rstrip(),float))
883        else:
884            self.asy2psmap = identity()
885        self.imageHandleQueue.put((None,))
886        self.asyfied = True
887
888class xasyDrawnItem(xasyItem):
889    """A base class for GUI items was drawn by the user. It combines a path, a pen, and a transform."""
890
891    def __init__(self, path, engine, pen=None, transform=identity(), key=None):
892        """Initialize the item with a path, pen, and transform"""
893        super().__init__(canvas=None, asyengine=engine)
894        if pen is None:
895            pen = asyPen()
896        self.path = path
897        self.path.asyengine = engine
898        self.asyfied = True
899        self.pen = pen
900        self._asyengine = engine
901        self.rawIdentifier = ''
902        self.transfKey = key
903        self.transfKeymap = {self.transfKey: [transform]}
904
905    @property
906    def asyengine(self):
907        return self._asyengine
908
909    @asyengine.setter
910    def asyengine(self, value: AsymptoteEngine):
911        self._asyengine = value
912        self.path.asyengine = value
913
914    def setKey(self, newKey=None):
915        transform = self.transfKeymap[self.transfKey][0]
916
917        self.transfKey = newKey
918        self.transfKeymap = {self.transfKey: [transform]}
919
920    def generateDrawObjects(self, forceUpdate=False):
921        raise NotImplementedError
922
923    def appendPoint(self, point, link=None):
924        """Append a point to the path. If the path is cyclic, add this point before the 'cycle' node."""
925        if self.path.nodeSet[-1] == 'cycle':
926            self.path.nodeSet[-1] = point
927            self.path.nodeSet.append('cycle')
928        else:
929            self.path.nodeSet.append(point)
930        self.path.computed = False
931        self.asyfied = False
932        if len(self.path.nodeSet) > 1 and link is not None:
933            self.path.linkSet.append(link)
934
935    def clearTransform(self):
936        """Reset the item's transform"""
937        self.transform = [identity()]
938        self.asyfied = False
939
940    def removeLastPoint(self):
941        """Remove the last point in the path. If the path is cyclic, remove the node before the 'cycle' node."""
942        if self.path.nodeSet[-1] == 'cycle':
943            del self.path.nodeSet[-2]
944        else:
945            del self.path.nodeSet[-1]
946        del self.path.linkSet[-1]
947        self.path.computed = False
948        self.asyfied = False
949
950    def setLastPoint(self, point):
951        """Modify the last point in the path. If the path is cyclic, modify the node before the 'cycle' node."""
952        if self.path.nodeSet[-1] == 'cycle':
953            self.path.nodeSet[-2] = point
954        else:
955            self.path.nodeSet[-1] = point
956        self.path.computed = False
957        self.asyfied = False
958
959
960class xasyShape(xasyDrawnItem):
961    """An outlined shape drawn on the GUI"""
962    def __init__(self, path, asyengine, pen=None, transform=identity()):
963        """Initialize the shape with a path, pen, and transform"""
964        super().__init__(path=path, engine=asyengine, pen=pen, transform=transform)
965
966    def getObjectCode(self, asy2psmap=identity()):
967        return 'draw(KEY="{0}",{1},{2});'.format(self.transfKey, self.path.getCode(asy2psmap), self.pen.getCode())+'\n\n'
968
969    def getTransformCode(self, asy2psmap=identity()):
970        transf = self.transfKeymap[self.transfKey][0]
971        if transf == identity():
972            return ''
973        else:
974            return xasyItem.setKeyFormatStr.format(self.transfKey, transf.getCode(asy2psmap))+'\n'
975
976    def generateDrawObjects(self, forceUpdate=False):
977        if self.path.containsCurve:
978            self.path.computeControls()
979        transf = self.transfKeymap[self.transfKey][0]
980
981        newObj = DrawObject(self.path.toQPainterPath(), None, drawOrder=0, transform=transf, pen=self.pen,
982                            key=self.transfKey)
983        newObj.originalObj = self
984        newObj.setParent(self)
985        return [newObj]
986
987    def __str__(self):
988        """Create a string describing this shape"""
989        return "xasyShape code:{:s}".format("\n\t".join(self.getCode().splitlines()))
990
991
992class xasyFilledShape(xasyShape):
993    """A filled shape drawn on the GUI"""
994
995    def __init__(self, path, asyengine, pen=None, transform=identity()):
996        """Initialize this shape with a path, pen, and transform"""
997        if path.nodeSet[-1] != 'cycle':
998            raise Exception("Filled paths must be cyclic")
999        super().__init__(path, asyengine, pen, transform)
1000
1001    def getObjectCode(self, asy2psmap=identity()):
1002        return 'fill(KEY="{0}",{1},{2});'.format(self.transfKey, self.path.getCode(asy2psmap), self.pen.getCode())+'\n\n'
1003
1004    def generateDrawObjects(self, forceUpdate=False):
1005        if self.path.containsCurve:
1006            self.path.computeControls()
1007        newObj = DrawObject(self.path.toQPainterPath(), None, drawOrder=0, transform=self.transfKeymap[self.transfKey][0],
1008                            pen=self.pen, key=self.transfKey, fill=True)
1009        newObj.originalObj = self
1010        newObj.setParent(self)
1011        return [newObj]
1012
1013    def __str__(self):
1014        """Return a string describing this shape"""
1015        return "xasyFilledShape code:{:s}".format("\n\t".join(self.getCode().splitlines()))
1016
1017
1018class xasyText(xasyItem):
1019    """Text created by the GUI"""
1020
1021    def __init__(self, text, location, asyengine, pen=None, transform=yflip(), key=None, align=None, fontsize:int=None):
1022        """Initialize this item with text, a location, pen, and transform"""
1023        super().__init__(asyengine=asyengine)
1024        if pen is None:
1025            pen = asyPen(asyengine=asyengine)
1026        if pen.asyEngine is None:
1027            pen.asyEngine = asyengine
1028        self.label = asyLabel(text, location, pen, align, fontSize=fontsize)
1029        # self.transform = [transform]
1030        self.transfKey = key
1031        self.transfKeymap = {self.transfKey: [transform]}
1032        self.asyfied = False
1033        self.onCanvas = None
1034
1035    def setKey(self, newKey=None):
1036        transform = self.transfKeymap[self.transfKey][0]
1037
1038        self.transfKey = newKey
1039        self.transfKeymap = {self.transfKey: [transform]}
1040
1041    def getTransformCode(self, asy2psmap=yflip()):
1042        transf = self.transfKeymap[self.transfKey][0]
1043        if transf == yflip():
1044            # return xasyItem.setKeyAloneFormatStr.format(self.transfKey)
1045            return ''
1046        else:
1047            return xasyItem.setKeyFormatStr.format(self.transfKey, transf.getCode(asy2psmap))+"\n"
1048
1049    def getObjectCode(self, asy2psmap=yflip()):
1050        return 'label(KEY="{0}",{1});'.format(self.transfKey, self.label.getCode(asy2psmap))+'\n'
1051
1052    def generateDrawObjects(self, forceUpdate=False):
1053        self.asyfy(forceUpdate)
1054        return self.drawObjects
1055
1056    def getBoundingBox(self):
1057        self.asyfy()
1058        return self.imageList[0].bbox
1059
1060    def __str__(self):
1061        return "xasyText code:{:s}".format("\n\t".join(self.getCode().splitlines()))
1062
1063
1064class xasyScript(xasyItem):
1065    """A set of images create from asymptote code. It is always deconstructed."""
1066
1067    def __init__(self, canvas, engine, script="", transforms=None, transfKeyMap=None):
1068        """Initialize this script item"""
1069        super().__init__(canvas, asyengine=engine)
1070        if transfKeyMap is not None:
1071            self.transfKeymap = transfKeyMap
1072        else:
1073            self.transfKeymap = {}
1074
1075        self.script = script
1076        self.key2imagemap = {}
1077        self.namedUnsetKeys = {}
1078        self.keyPrefix = ''
1079        self.scriptAsyfied = False
1080        self.updatedPrefix = True
1081
1082    def clearTransform(self):
1083        """Reset the transforms for each of the deconstructed images"""
1084        # self.transform = [identity()] * len(self.imageList)
1085        keyCount = {}
1086
1087        for im in self.imageList:
1088            if im.key not in keyCount.keys():
1089                keyCount[im.key] = 1
1090            else:
1091                keyCount[im.key] += 1
1092
1093        for key in keyCount:
1094            self.transfKeymap[key] = [identity()] * keyCount[key]
1095
1096    def getMaxKeyCounter(self):
1097        maxCounter = -1
1098        for key in self.transfKeymap:
1099            testNum = re.match(r'^x(\d+)$', key)
1100            if testNum is not None:
1101                maxCounter = max(maxCounter, int(testNum.group(1)))
1102        return maxCounter + 1
1103
1104    def getTransformCode(self, asy2psmap=identity()):
1105        with io.StringIO() as rawAsyCode:
1106            if self.transfKeymap:
1107                for key in self.transfKeymap.keys():
1108                    val = self.transfKeymap[key]
1109
1110                    writeval = list(reversed(val))
1111                    # need to map all transforms in a list if there is any non-identity
1112                    # unfortunately, have to check all transformations in the list.
1113                    while not all(checktransf == identity() for checktransf in writeval) and writeval:
1114                        transf = writeval.pop()
1115                        if transf.deleted:
1116                            rawAsyCode.write(xasyItem.setKeyFormatStr.format(key, transf.getCode(asy2psmap)) + '\n//')
1117                        if transf == identity() and not transf.deleted:
1118                            rawAsyCode.write(xasyItem.setKeyAloneFormatStr.format(key))
1119                        else:
1120                            rawAsyCode.write(xasyItem.setKeyFormatStr.format(key, transf.getCode(asy2psmap)))
1121                        rawAsyCode.write('\n')
1122            result = rawAsyCode.getvalue()
1123        return result
1124
1125    def findNonIdKeys(self):
1126        return {key for key in self.transfKeymap if not all(transf == identity() for transf in self.transfKeymap[key]) }
1127
1128    def getObjectCode(self, asy2psmap=identity()):
1129        numeric=r'([-+]?(?:(?:\d*\.\d+)|(?:\d+\.?)))'
1130        rSize=re.compile("size\(\("+numeric+","+numeric+","+numeric+","
1131                         +numeric+","+numeric+","+numeric+"\)\); "+
1132                         self.resizeComment)
1133
1134        newScript = self.getReplacedKeysCode(self.findNonIdKeys())
1135        with io.StringIO() as rawAsyCode:
1136            for line in newScript.splitlines():
1137                if(rSize.match(line)):
1138                    self.asySize=line.rstrip()+'\n'
1139                else:
1140                    raw_line = line.rstrip().replace('\t', ' ' * 4)
1141                    rawAsyCode.write(raw_line + '\n')
1142
1143            self.updatedCode = rawAsyCode.getvalue()
1144            return self.updatedCode
1145
1146    def setScript(self, script):
1147        """Sets the content of the script item."""
1148        self.script = script
1149        self.updateCode()
1150
1151    def setKeyPrefix(self, newPrefix=''):
1152        self.keyPrefix = newPrefix
1153        self.updatedPrefix = False
1154
1155    def getReplacedKeysCode(self, key2replace: set=None) -> str:
1156        keylist = {}
1157        prefix = ''
1158
1159        key2replaceSet = self.unsetKeys if key2replace is None else \
1160                        self.unsetKeys & key2replace
1161
1162        linenum2key = {}
1163
1164        if not self.updatedPrefix:
1165            prefix = self.keyPrefix
1166
1167        for key in key2replaceSet:
1168            actualkey = key
1169
1170            key = key.split(':')[0]
1171            raw_parsed = xu.tryParseKey(key)
1172            assert raw_parsed is not None
1173            line, col = [int(val) for val in raw_parsed.groups()]
1174            if line not in keylist:
1175                keylist[line] = set()
1176            keylist[line].add(col)
1177            linenum2key[(line, col)] = actualkey
1178            self.unsetKeys.discard(key)
1179
1180
1181        raw_code_lines = self.script.splitlines()
1182        with io.StringIO() as raw_str:
1183            for i_0 in range(len(raw_code_lines)):
1184                i = i_0 + self.lineOffset
1185                curr_str = raw_code_lines[i_0]
1186                if i + 1 in keylist.keys():
1187                    # this case, we have a key.
1188                    with io.StringIO() as raw_line:
1189                        for j in range(len(curr_str)):
1190                            raw_line.write(curr_str[j])
1191                            if j + 1 in keylist[i + 1]:
1192                                # at this point, replace keys with xkey
1193                                raw_line.write('KEY="{0:s}",'.format(linenum2key[(i + 1, j + 1)]))
1194                                self.userKeys.add(linenum2key[(i + 1, j + 1)])
1195                        curr_str = raw_line.getvalue()
1196                # else, skip and just write the line.
1197                raw_str.write(curr_str + '\n')
1198            return raw_str.getvalue()
1199
1200    def getUnusedKey(self, oldkey) -> str:
1201        baseCounter = 0
1202        newKey = oldkey
1203        while newKey in self.userKeys:
1204            newKey = oldkey + ':' + str(baseCounter)
1205            baseCounter += 1
1206        return newKey
1207
1208    def asyfy(self, keyOnly=False):
1209        """Generate the list of images described by this object and adjust the length of the transform list."""
1210        super().asyfy()
1211
1212        # Id --> Transf --> asy-fied --> Transf
1213        # Transf should keep the original, raw transformation
1214        # but for all new drawn objects - assign Id as transform.
1215
1216        if self.scriptAsyfied:
1217            return
1218
1219        keyCount = {}
1220        settedKey = {}
1221
1222        for im in self.imageList:
1223            if im.key in self.unsetKeys and im.key not in settedKey.keys():
1224                oldkey = im.key
1225                self.unsetKeys.remove(im.key)
1226                im.key = self.getUnusedKey(im.key)
1227                self.unsetKeys.add(im.key)
1228
1229                for drawobj in self.drawObjectsMap[oldkey]:
1230                    drawobj.key = im.key
1231
1232                self.drawObjectsMap[im.key] = self.drawObjectsMap[oldkey]
1233                self.drawObjectsMap.pop(oldkey)
1234
1235                settedKey[oldkey] = im.key
1236            elif im.key in settedKey.keys():
1237                im.key = settedKey[im.key]
1238
1239            if im.key not in keyCount.keys():
1240                keyCount[im.key] = 1
1241            else:
1242                keyCount[im.key] += 1
1243
1244            if im.key not in self.key2imagemap.keys():
1245                self.key2imagemap[im.key] = [im]
1246            else:
1247                self.key2imagemap[im.key].append(im)
1248
1249
1250
1251        for key in keyCount:
1252            if key not in self.transfKeymap.keys():
1253                self.transfKeymap[key] = [identity()] * keyCount[key]
1254            else:
1255                while len(self.transfKeymap[key]) < keyCount[key]:
1256                    self.transfKeymap[key].append(identity())
1257
1258                # while len(self.transfKeymap[key]) > keyCount[key]:
1259                    # self.transfKeymap[key].pop()
1260
1261        # change of basis
1262        for keylist in self.transfKeymap.values():
1263            for i in range(len(keylist)):
1264                if keylist[i] != identity():
1265                    keylist[i] = self.asy2psmap * keylist[i] * self.asy2psmap.inverted()
1266
1267        self.updateCode()
1268        self.scriptAsyfied = True
1269
1270    def generateDrawObjects(self, forceUpdate=False):
1271        self.asyfy(forceUpdate)
1272        return self.drawObjects
1273
1274    def __str__(self):
1275        """Return a string describing this script"""
1276        retVal = "xasyScript\n\tTransforms:\n"
1277        for xform in self.transform:
1278            retVal += "\t" + str(xform) + "\n"
1279        retVal += "\tCode Ommitted"
1280        return retVal
1281
1282
1283class DrawObject(Qc.QObject):
1284    def __init__(self, drawObject, mainCanvas=None, transform=identity(), btmRightanchor=Qc.QPointF(0, 0),
1285                 drawOrder=(-1, -1), pen=None, key=None, parentObj=None, fill=False, keyIndex=0):
1286        super().__init__()
1287        self.drawObject = drawObject
1288        self.mainCanvas = mainCanvas
1289        self.pTransform = transform
1290        self.baseTransform = transform
1291        self.drawOrder = drawOrder
1292        self.btmRightAnchor = btmRightanchor
1293        self.originalObj = parentObj
1294        self.explicitBoundingBox = None
1295        self.useCanvasTransformation = False
1296        self.key = key
1297        self.cachedSvgImg = None
1298        self.cachedDPI = None
1299        self.maxDPI=0
1300        self.keyIndex = keyIndex
1301        self.pen = pen
1302        self.fill = fill
1303
1304    def getInteriorScrTransform(self, transform):
1305        """Generates the transform with Interior transform applied beforehand."""
1306        if isinstance(transform, Qg.QTransform):
1307            transform = asyTransform.fromQTransform(transform)
1308        return self.transform * transform * self.baseTransform.inverted()
1309
1310    @property
1311    def transform(self):
1312        return self.pTransform
1313
1314    @transform.setter
1315    def transform(self, value):
1316        self.pTransform = value
1317
1318    def setBoundingBoxPs(self, bbox):
1319        l, b, r, t = bbox
1320        self.explicitBoundingBox = Qc.QRectF(Qc.QPointF(l, b), Qc.QPointF(r, t))
1321        # self.explicitBoundingBox = Qc.QRectF(0, 0, 100, 100)
1322
1323    @property
1324    def boundingBox(self):
1325        if self.explicitBoundingBox is not None:
1326            testBbox = self.explicitBoundingBox
1327        else:
1328            if isinstance(self.drawObject, Qg.QImage):
1329                testBbox = self.drawObject.rect()
1330                testBbox.moveTo(self.btmRightAnchor.toPoint())
1331            elif isinstance(self.drawObject, Qg.QPainterPath):
1332                testBbox = self.baseTransform.toQTransform().mapRect(self.drawObject.boundingRect())
1333            else:
1334                raise TypeError('drawObject is not a valid type!')
1335        pointList = [self.getScreenTransform().toQTransform().map(point) for point in [
1336            testBbox.topLeft(), testBbox.topRight(), testBbox.bottomLeft(), testBbox.bottomRight()
1337        ]]
1338        return Qg.QPolygonF(pointList).boundingRect()
1339
1340    @property
1341    def localBoundingBox(self):
1342        testBbox = self.drawObject.rect()
1343        testBbox.moveTo(self.btmRightAnchor.toPoint())
1344        return testBbox
1345
1346    def getScreenTransform(self):
1347        scrTransf = self.baseTransform.toQTransform().inverted()[0] * self.pTransform.toQTransform()
1348        return asyTransform.fromQTransform(scrTransf)
1349
1350    def draw(self, additionalTransformation=None, applyReverse=False, canvas: Qg.QPainter=None, dpi=300):
1351        if canvas is None:
1352            canvas = self.mainCanvas
1353        if additionalTransformation is None:
1354            additionalTransformation = Qg.QTransform()
1355
1356        assert canvas.isActive()
1357
1358        canvas.save()
1359        if self.pen:
1360            oldPen = Qg.QPen(canvas.pen())
1361            canvas.setPen(self.pen.toQPen())
1362        else:
1363            oldPen = Qg.QPen()
1364
1365        if not applyReverse:
1366            canvas.setTransform(additionalTransformation, True)
1367            canvas.setTransform(self.transform.toQTransform(), True)
1368        else:
1369            canvas.setTransform(self.transform.toQTransform(), True)
1370            canvas.setTransform(additionalTransformation, True)
1371
1372        canvas.setTransform(self.baseTransform.toQTransform().inverted()[0], True)
1373
1374        if isinstance(self.drawObject, Qg.QImage):
1375            canvas.drawImage(self.explicitBoundingBox, self.drawObject)
1376        elif isinstance(self.drawObject, xs.SvgObject):
1377            threshold = 1.44
1378
1379            if self.cachedDPI is None or self.cachedSvgImg is None \
1380               or dpi > self.maxDPI*threshold:
1381                self.cachedDPI = dpi
1382                self.maxDPI=max(self.maxDPI,dpi)
1383                self.cachedSvgImg = self.drawObject.render(dpi)
1384
1385            canvas.drawImage(self.explicitBoundingBox, self.cachedSvgImg)
1386        elif isinstance(self.drawObject, Qs.QSvgRenderer):
1387            self.drawObject.render(canvas, self.explicitBoundingBox)
1388        elif isinstance(self.drawObject, Qg.QPainterPath):
1389            path = self.baseTransform.toQTransform().map(self.drawObject)
1390            if self.fill:
1391                if self.pen:
1392                    brush = self.pen.toQPen().brush()
1393                else:
1394                    brush = Qg.QBrush()
1395                canvas.fillPath(path, brush)
1396            else:
1397                canvas.drawPath(path)
1398
1399        if self.pen:
1400            canvas.setPen(oldPen)
1401        canvas.restore()
1402
1403    def collide(self, coords, canvasCoordinates=True):
1404        # modify these values to grow/shrink the fuzz.
1405        fuzzTolerance = 1
1406        marginGrowth = 1
1407        leftMargin = marginGrowth if self.boundingBox.width() < fuzzTolerance else 0
1408        topMargin = marginGrowth if self.boundingBox.height() < fuzzTolerance else 0
1409
1410        newMargin = Qc.QMarginsF(leftMargin, topMargin, leftMargin, topMargin)
1411        return self.boundingBox.marginsAdded(newMargin).contains(coords)
1412
1413    def getID(self):
1414        return self.originalObj
1415