def make_raster( self, nodes, bounds, width=None, height=None, bitmap=False, step_x=1, step_y=1, keep_ratio=False, recursion=0, ): """ Make Raster turns an iterable of elements and a bounds into an image of the designated size, taking into account the step size. The physical pixels in the image is reduced by the step size then the matrix for the element is scaled up by the same amount. This makes step size work like inverse dpi and correctly sets the image scale to the step scale for 1:1 sizes independent of the scale. This function requires both wxPython and Pillow. @param nodes: elements to render. @param bounds: bounds of those elements for the viewport. @param width: desired width of the resulting raster @param height: desired height of the resulting raster @param bitmap: bitmap to use rather than provisioning @param step: raster step rate, int scale rate of the image. @param keepratio: get a picture with the same height / width ratio as the original @return: """ if bounds is None: return None xxmin = float("inf") yymin = float("inf") xxmax = -float("inf") yymax = -float("inf") # print ("Recursion=%d" % recursion) if not isinstance(nodes, (tuple, list)): mynodes = [nodes] else: mynodes = nodes if recursion == 0: # Do it only once... textnodes = [] for item in mynodes: if item.type == "elem text": if item.text.width == 0 or item.text.height == 0: textnodes.append(item) if len(textnodes) > 0: # print ("Invalid textnodes found, call me again...") self.make_raster( nodes=textnodes, bounds=bounds, width=width, height=height, bitmap=bitmap, step_x=step_x, step_y=step_y, keep_ratio=keep_ratio, recursion=1, ) for item in mynodes: bb = item.bounds # if item.type == "elem text": # print ("Bounds for text: %.1f, %.1f, %.1f, %.1f, w=%.1f, h=%.1f)" % (bb[0], bb[1], bb[2], bb[3], item.text.width, item.text.height)) if bb[0] < xxmin: xxmin = bb[0] if bb[1] < yymin: yymin = bb[1] if bb[2] > xxmax: xxmax = bb[2] if bb[3] > yymax: yymax = bb[3] xmin = xxmin ymin = yymin xmax = xxmax ymax = yymax xmax = ceil(xmax) ymax = ceil(ymax) xmin = floor(xmin) ymin = floor(ymin) # print ("Bounds: %.1f, %.1f, %.1f, %.1f, Mine: %.1f, %.1f, %.1f, %.1f)" % (xmin, ymin, xmax, ymax, xxmin, yymin, xxmax, yymax)) image_width = int(xmax - xmin) if image_width == 0: image_width = 1 image_height = int(ymax - ymin) if image_height == 0: image_height = 1 if width is None: width = image_width if height is None: height = image_height # Scale physical image down by step amount. width /= float(step_x) height /= float(step_y) width = int(ceil(abs(width))) height = int(ceil(abs(height))) if width <= 0: width = 1 if height <= 0: height = 1 bmp = wx.Bitmap(width, height, 32) dc = wx.MemoryDC() dc.SelectObject(bmp) dc.SetBackground(wx.WHITE_BRUSH) dc.Clear() matrix = Matrix() matrix.post_translate(-xmin, -ymin) # Scale affine matrix up by step amount scaled down. scale_x = width / float(image_width) scale_y = height / float(image_height) if keep_ratio: scale_x = min(scale_x, scale_y) scale_y = scale_x matrix.post_scale(scale_x, scale_y) gc = wx.GraphicsContext.Create(dc) gc.SetInterpolationQuality(wx.INTERPOLATION_BEST) gc.PushState() if not matrix.is_identity(): gc.ConcatTransform( wx.GraphicsContext.CreateMatrix(gc, ZMatrix(matrix))) if not isinstance(nodes, (list, tuple)): nodes = [nodes] gc.SetBrush(wx.WHITE_BRUSH) gc.DrawRectangle(xmin - 1, ymin - 1, xmax + 1, ymax + 1) self.render(nodes, gc, draw_mode=DRAW_MODE_CACHE | DRAW_MODE_VARIABLES) img = bmp.ConvertToImage() buf = img.GetData() image = Image.frombuffer("RGB", tuple(bmp.GetSize()), bytes(buf), "raw", "RGB", 0, 1) gc.PopState() dc.SelectObject(wx.NullBitmap) gc.Destroy() del dc if bitmap: return bmp # for item in mynodes: # bb = item.bounds # if item.type == "elem text": # print ("Afterwards Bounds for text: %.1f, %.1f, %.1f, %.1f, w=%.1f, h=%.1f)" % (bb[0], bb[1], bb[2], bb[3], item.text.width, item.text.height)) return image
class ImageNode(Node): """ ImageNode is the bootstrapped node type for the 'elem image' type. ImageNode contains a main matrix, main image. A processed image and a processed matrix. The processed matrix must be concated with the main matrix to be accurate. """ def __init__( self, image=None, matrix=None, overscan=None, direction=None, dpi=500, operations=None, **kwargs, ): super(ImageNode, self).__init__(type="elem image", **kwargs) self.__formatter = "{element_type} {width}x{height}" if "href" in kwargs: self.matrix = Matrix() try: from PIL import Image as PILImage self.image = PILImage.open(kwargs["href"]) if "x" in kwargs: self.matrix.post_translate_x(kwargs["x"]) if "y" in kwargs: self.matrix.post_translate_x(kwargs["y"]) real_width, real_height = self.image.size declared_width, declared_height = real_width, real_height if "width" in kwargs: declared_width = kwargs["width"] if "height" in kwargs: declared_height = kwargs["height"] try: sx = declared_width / real_width sy = declared_height / real_height self.matrix.post_scale(sx, sy) except ZeroDivisionError: pass except ImportError: self.image = None else: self.image = image self.matrix = matrix self.processed_image = None self.processed_matrix = None self.process_image_failed = False self.text = None self._needs_update = False self._update_thread = None self._update_lock = threading.Lock() self.settings = kwargs self.overscan = overscan self.direction = direction self.dpi = dpi self.step_x = None self.step_y = None self.lock = False self.invert = False self.red = 1.0 self.green = 1.0 self.blue = 1.0 self.lightness = 1.0 self.view_invert = False self.dither = True self.dither_type = "Floyd-Steinberg" if operations is None: operations = list() self.operations = operations def __copy__(self): return ImageNode( image=self.image, matrix=copy(self.matrix), overscan=self.overscan, direction=self.direction, dpi=self.dpi, operations=self.operations, **self.settings, ) def __repr__(self): return "%s('%s', %s, %s)" % ( self.__class__.__name__, self.type, str(self.image), str(self._parent), ) @property def active_image(self): if self.processed_image is not None: return self.processed_image else: return self.image @property def active_matrix(self): if self.processed_matrix is None: return self.matrix return self.processed_matrix * self.matrix def preprocess(self, context, matrix, commands): """ Preprocess step during the cut planning stages. We require a context to calculate the correct step values relative to the device """ self.step_x, self.step_y = context.device.dpi_to_steps(self.dpi) self.matrix *= matrix self._bounds_dirty = True self.process_image() @property def bounds(self): if self._bounds_dirty: image_width, image_height = self.active_image.size matrix = self.active_matrix x0, y0 = matrix.point_in_matrix_space((0, 0)) x1, y1 = matrix.point_in_matrix_space((image_width, image_height)) x2, y2 = matrix.point_in_matrix_space((0, image_height)) x3, y3 = matrix.point_in_matrix_space((image_width, 0)) self._bounds_dirty = False self._bounds = ( min(x0, x1, x2, x3), min(y0, y1, y2, y3), max(x0, x1, x2, x3), max(y0, y1, y2, y3), ) return self._bounds def default_map(self, default_map=None): default_map = super(ImageNode, self).default_map(default_map=default_map) default_map.update(self.settings) image = self.active_image default_map["width"] = image.width default_map["height"] = image.height default_map["element_type"] = "Image" default_map["matrix"] = self.matrix default_map["dpi"] = self.dpi default_map["overscan"] = self.overscan default_map["direction"] = self.direction return default_map def drop(self, drag_node, modify=True): # Dragging element into element. if drag_node.type.startswith("elem"): if modify: self.insert_sibling(drag_node) return True return False def revalidate_points(self): bounds = self.bounds if bounds is None: return if len(self._points) < 9: self._points.extend([None] * (9 - len(self._points))) self._points[0] = [bounds[0], bounds[1], "bounds top_left"] self._points[1] = [bounds[2], bounds[1], "bounds top_right"] self._points[2] = [bounds[0], bounds[3], "bounds bottom_left"] self._points[3] = [bounds[2], bounds[3], "bounds bottom_right"] cx = (bounds[0] + bounds[2]) / 2 cy = (bounds[1] + bounds[3]) / 2 self._points[4] = [cx, cy, "bounds center_center"] self._points[5] = [cx, bounds[1], "bounds top_center"] self._points[6] = [cx, bounds[3], "bounds bottom_center"] self._points[7] = [bounds[0], cy, "bounds center_left"] self._points[8] = [bounds[2], cy, "bounds center_right"] def update_point(self, index, point): return False def add_point(self, point, index=None): return False def update(self, context): self._needs_update = True self.text = "Processing..." context.signal("refresh_scene", "Scene") if self._update_thread is None: def clear(result): if self.process_image_failed: self.text = "Process image could not exist in memory." else: self.text = None self._needs_update = False self._update_thread = None context.signal("refresh_scene", "Scene") context.signal("image updated", self) self.processed_image = None self.processed_matrix = None self._update_thread = context.threaded( self.process_image_thread, result=clear, daemon=True ) def process_image_thread(self): while self._needs_update: self._needs_update = False self.process_image() # Unset cache. self.wx_bitmap_image = None self.cache = None def process_image(self): if self.step_x is None: step = UNITS_PER_INCH / self.dpi self.step_x = step self.step_y = step from PIL import Image, ImageEnhance, ImageFilter, ImageOps from meerk40t.image.actualize import actualize from meerk40t.image.imagetools import dither image = self.image main_matrix = self.matrix r = self.red * 0.299 g = self.green * 0.587 b = self.blue * 0.114 v = self.lightness c = r + g + b try: c /= v r = r / c g = g / c b = b / c except ZeroDivisionError: pass if image.mode != "L": image = image.convert("RGB") image = image.convert("L", matrix=[r, g, b, 1.0]) if self.invert: image = image.point(lambda e: 255 - e) # Calculate device real step. step_x, step_y = self.step_x, self.step_y if ( main_matrix.a != step_x or main_matrix.b != 0.0 or main_matrix.c != 0.0 or main_matrix.d != step_y ): try: image, actualized_matrix = actualize( image, main_matrix, step_x=step_x, step_y=step_y, inverted=self.invert, ) except (MemoryError, DecompressionBombError): self.process_image_failed = True return else: actualized_matrix = Matrix(main_matrix) if self.invert: empty_mask = image.convert("L").point(lambda e: 0 if e == 0 else 255) else: empty_mask = image.convert("L").point(lambda e: 0 if e == 255 else 255) # Process operations. for op in self.operations: name = op["name"] if name == "crop": try: if op["enable"] and op["bounds"] is not None: crop = op["bounds"] left = int(crop[0]) upper = int(crop[1]) right = int(crop[2]) lower = int(crop[3]) image = image.crop((left, upper, right, lower)) except KeyError: pass elif name == "edge_enhance": try: if op["enable"]: if image.mode == "P": image = image.convert("L") image = image.filter(filter=ImageFilter.EDGE_ENHANCE) except KeyError: pass elif name == "auto_contrast": try: if op["enable"]: if image.mode not in ("RGB", "L"): # Auto-contrast raises NotImplementedError if P # Auto-contrast raises OSError if not RGB, L. image = image.convert("L") image = ImageOps.autocontrast(image, cutoff=op["cutoff"]) except KeyError: pass elif name == "tone": try: if op["enable"] and op["values"] is not None: if image.mode == "L": image = image.convert("P") tone_values = op["values"] if op["type"] == "spline": spline = ImageNode.spline(tone_values) else: tone_values = [q for q in tone_values if q is not None] spline = ImageNode.line(tone_values) if len(spline) < 256: spline.extend([255] * (256 - len(spline))) if len(spline) > 256: spline = spline[:256] image = image.point(spline) if image.mode != "L": image = image.convert("L") except KeyError: pass elif name == "contrast": try: if op["enable"]: if op["contrast"] is not None and op["brightness"] is not None: contrast = ImageEnhance.Contrast(image) c = (op["contrast"] + 128.0) / 128.0 image = contrast.enhance(c) brightness = ImageEnhance.Brightness(image) b = (op["brightness"] + 128.0) / 128.0 image = brightness.enhance(b) except KeyError: pass elif name == "gamma": try: if op["enable"] and op["factor"] is not None: if image.mode == "L": gamma_factor = float(op["factor"]) def crimp(px): px = int(round(px)) if px < 0: return 0 if px > 255: return 255 return px if gamma_factor == 0: gamma_lut = [0] * 256 else: gamma_lut = [ crimp(pow(i / 255, (1.0 / gamma_factor)) * 255) for i in range(256) ] image = image.point(gamma_lut) if image.mode != "L": image = image.convert("L") except KeyError: pass elif name == "unsharp_mask": try: if ( op["enable"] and op["percent"] is not None and op["radius"] is not None and op["threshold"] is not None ): unsharp = ImageFilter.UnsharpMask( radius=op["radius"], percent=op["percent"], threshold=op["threshold"], ) image = image.filter(unsharp) except (KeyError, ValueError): # Value error if wrong type of image. pass elif name == "halftone": try: if op["enable"]: image = RasterScripts.halftone( image, sample=op["sample"], angle=op["angle"], oversample=op["oversample"], black=op["black"], ) except KeyError: pass if empty_mask is not None: background = Image.new(image.mode, image.size, "white") background.paste(image, mask=empty_mask) image = background # Mask exists use it to remove any pixels that were pure reject. if self.dither and self.dither_type is not None: if self.dither_type != "Floyd-Steinberg": image = dither(image, self.dither_type) image = image.convert("1") inverted_main_matrix = Matrix(main_matrix).inverse() self.processed_matrix = actualized_matrix * inverted_main_matrix self.processed_image = image # self.matrix = actualized_matrix self.altered() self.process_image_failed = False @staticmethod def line(p): N = len(p) - 1 try: m = [(p[i + 1][1] - p[i][1]) / (p[i + 1][0] - p[i][0]) for i in range(0, N)] except ZeroDivisionError: m = [1] * N # b = y - mx b = [p[i][1] - (m[i] * p[i][0]) for i in range(0, N)] r = list() for i in range(0, p[0][0]): r.append(0) for i in range(len(p) - 1): x0 = p[i][0] x1 = p[i + 1][0] range_list = [int(round((m[i] * x) + b[i])) for x in range(x0, x1)] r.extend(range_list) for i in range(p[-1][0], 256): r.append(255) r.append(round(int(p[-1][1]))) return r @staticmethod def spline(p): """ Spline interpreter. Returns all integer locations between different spline interpolation values @param p: points to be quad spline interpolated. @return: integer y values for given spline points. """ try: N = len(p) - 1 w = [(p[i + 1][0] - p[i][0]) for i in range(0, N)] h = [(p[i + 1][1] - p[i][1]) / w[i] for i in range(0, N)] ftt = ( [0] + [3 * (h[i + 1] - h[i]) / (w[i + 1] + w[i]) for i in range(0, N - 1)] + [0] ) A = [(ftt[i + 1] - ftt[i]) / (6 * w[i]) for i in range(0, N)] B = [ftt[i] / 2 for i in range(0, N)] C = [h[i] - w[i] * (ftt[i + 1] + 2 * ftt[i]) / 6 for i in range(0, N)] D = [p[i][1] for i in range(0, N)] except ZeroDivisionError: return list(range(256)) r = list() for i in range(0, p[0][0]): r.append(0) for i in range(len(p) - 1): a = p[i][0] b = p[i + 1][0] r.extend( int( round( A[i] * (x - a) ** 3 + B[i] * (x - a) ** 2 + C[i] * (x - a) + D[i] ) ) for x in range(a, b) ) for i in range(p[-1][0], 256): r.append(255) r.append(round(int(p[-1][1]))) return r def as_path(self): image_width, image_height = self.active_image.size matrix = self.active_matrix x0, y0 = matrix.point_in_matrix_space((0, 0)) x1, y1 = matrix.point_in_matrix_space((0, image_height)) x2, y2 = matrix.point_in_matrix_space((image_width, image_height)) x3, y3 = matrix.point_in_matrix_space((image_width, 0)) return abs(Path(Polygon((x0,y0), (x1,y1), (x2,y2), (x3,y3), (x0,y0))))
class Widget(list): """ Widgets are drawable, interaction objects within the scene. They have their own space, matrix, orientation, and processing of events. """ def __init__( self, scene, left: float = None, top: float = None, right: float = None, bottom: float = None, all: bool = False, ): """ All produces a widget of infinite space rather than finite space. """ assert scene.__class__.__name__ == "Scene" list.__init__(self) self.matrix = Matrix() self.scene = scene self.parent = None self.properties = ORIENTATION_RELATIVE if all: # contains all points self.left = -float("inf") self.top = -float("inf") self.right = float("inf") self.bottom = float("inf") else: # contains no points self.left = float("inf") self.top = float("inf") self.right = -float("inf") self.bottom = -float("inf") if left is not None: self.left = left if right is not None: self.right = right if top is not None: self.top = top if bottom is not None: self.bottom = bottom def __str__(self): return "Widget(%f, %f, %f, %f)" % (self.left, self.top, self.right, self.bottom) def __repr__(self): return "%s(%f, %f, %f, %f)" % ( type(self).__name__, self.left, self.top, self.right, self.bottom, ) def hit(self): """ Default hit state delegates to child-widgets within the current object. """ return HITCHAIN_DELEGATE def draw(self, gc): """ Widget.draw() routine which concat's the widgets matrix and call the process_draw() function. """ # Concat if this is a thing. matrix = self.matrix gc.PushState() if matrix is not None and not matrix.is_identity(): gc.ConcatTransform( wx.GraphicsContext.CreateMatrix(gc, ZMatrix(matrix))) self.process_draw(gc) for i in range(len(self) - 1, -1, -1): widget = self[i] if not widget is None: widget.draw(gc) gc.PopState() def process_draw(self, gc): """ Overloaded function by derived widgets to process the drawing of this widget. """ pass def contains(self, x, y=None): """ Query whether the current point is contained within the current widget. """ if y is None: y = x.y x = x.x return self.left <= x <= self.right and self.top <= y <= self.bottom def event(self, window_pos=None, space_pos=None, event_type=None, nearest_snap=None): """ Default event which simply chains the event to the next hittable object. """ return RESPONSE_CHAIN def notify_added_to_parent(self, parent): """ Widget notify that calls scene notify. """ self.scene.notify_added_to_parent(parent) def notify_added_child(self, child): """ Widget notify that calls scene notify. """ self.scene.notify_added_child(child) def notify_removed_from_parent(self, parent): """ Widget notify that calls scene notify. """ self.scene.notify_removed_from_parent(parent) def notify_removed_child(self, child): """ Widget notify that calls scene notify. """ self.scene.notify_removed_child(child) def notify_moved_child(self, child): """ Widget notify that calls scene notify. """ self.scene.notify_moved_child(child) def add_widget(self, index=-1, widget=None, properties=0): """ Add a widget to the current widget. Adds at the particular index according to the properties. The properties can be used to trigger particular layouts or properties for the added widget. """ if len(self) == 0: last = self else: last = self[-1] if 0 <= index < len(self): self.insert(index, widget) else: self.append(widget) widget.parent = self self.layout_by_orientation(widget, last, properties) self.notify_added_to_parent(self) self.notify_added_child(widget) def translate(self, dx, dy): """ Move the current widget and all child widgets. """ if dx == 0 and dy == 0: return if isnan(dx) or isnan(dy) or isinf(dx) or isinf(dy): return self.translate_loop(dx, dy) def translate_loop(self, dx, dy): """ Loop the translation call to all child objects. """ if self.properties & ORIENTATION_ABSOLUTE != 0: return # Do not translate absolute oriented widgets. self.translate_self(dx, dy) for w in self: w.translate_loop(dx, dy) def translate_self(self, dx, dy): """ Perform the local translation of the current widget """ self.left += dx self.right += dx self.top += dy self.bottom += dy if self.parent is not None: self.notify_moved_child(self) def union_children_bounds(self, bounds=None): """ Find the bounds of the current widget and all child widgets. """ if bounds is None: bounds = [self.left, self.top, self.right, self.bottom] else: if bounds[0] > self.left: bounds[0] = self.left if bounds[1] > self.top: bounds[1] = self.top if bounds[2] < self.right: bounds[2] = self.left if bounds[3] < self.bottom: bounds[3] = self.bottom for w in self: w.union_children_bounds(bounds) return bounds @property def height(self): """ Height of the current widget. """ return self.bottom - self.top @property def width(self): """ Width of the current widget. """ return self.right - self.left def layout_by_orientation(self, widget, last, properties): """ Perform specific layout based on the properties given. ORIENTATION_ABSOLUTE places the widget exactly in the scene. ORIENTATION_NO_BUFFER nullifies any buffer between objects being laid out. ORIENTATION_RELATIVE lays out the added widget relative to the parent. ORIENTATION_GRID lays out the added widget in a DIM_MASK grid. ORIENTATION_VERTICAL lays the added widget below the reference widget. ORIENTATION_HORIZONTAL lays the added widget to the right of the reference widget. ORIENTATION_CENTERED lays out the added widget and within the parent and all child centered. """ if properties & ORIENTATION_ABSOLUTE != 0: return if properties & ORIENTATION_NO_BUFFER != 0: buffer = 0 else: buffer = BUFFER if (properties & ORIENTATION_MODE_MASK) == ORIENTATION_RELATIVE: widget.translate(self.left, self.top) return elif last is None: # orientation = origin widget.translate(self.left - widget.left, self.top - widget.top) elif (properties & ORIENTATION_GRID) != 0: dim = properties & ORIENTATION_DIM_MASK if (properties & ORIENTATION_VERTICAL) != 0: if dim == 0: # Vertical if self.height >= last.bottom - self.top + widget.height: # add to line widget.translate(last.left - widget.left, last.bottom - widget.top) else: # line return widget.translate(last.right - widget.left + buffer, self.top - widget.top) else: if dim == 0: # Horizontal if self.width >= last.right - self.left + widget.width: # add to line widget.translate(last.right - widget.left + buffer, last.top - widget.top) else: # line return widget.translate(self.left - widget.left, last.bottom - widget.top + buffer) elif (properties & ORIENTATION_HORIZONTAL) != 0: widget.translate(last.right - widget.left + buffer, last.top - widget.top) elif (properties & ORIENTATION_VERTICAL) != 0: widget.translate(last.left - widget.left, last.bottom - widget.top + buffer) if properties & ORIENTATION_CENTERED: self.center_children() def center_children(self): """ Centers the children of the current widget within the current widget. """ child_bounds = self.union_children_bounds() dx = self.left - (child_bounds[0] + child_bounds[2]) / 2.0 dy = self.top - (child_bounds[1] + child_bounds[3]) / 2.0 if dx != 0 and dy != 0: for w in self: w.translate_loop(dx, dy) def center_widget(self, x, y=None): """ Moves the current widget to center within the bounds of the children. """ if y is None: y = x.y x = x.x child_bounds = self.union_children_bounds() cx = (child_bounds[0] + child_bounds[2]) / 2.0 cy = (child_bounds[1] + child_bounds[3]) / 2.0 self.translate(x - cx, y - cy) def set_position(self, x, y=None): """ Sets the absolute position of this widget by moving it from its current position to given position. """ if y is None: y = x.y x = x.x dx = x - self.left dy = y - self.top self.translate(dx, dy) def remove_all_widgets(self): """ Remove all widgets from the current widget. """ for w in self: if w is None: continue w.parent = None w.notify_removed_from_parent(self) self.notify_removed_child(w) self.clear() try: self.scene.notify_tree_changed() except AttributeError: pass def remove_widget(self, widget=None): """ Remove the given widget from being a child of the current widget. """ if widget is None: return if isinstance(widget, Widget): self.remove(widget) elif isinstance(widget, int): index = widget widget = self[index] del self[index] widget.parent = None widget.notify_removed_from_parent(self) self.notify_removed_child(widget) try: self.scene.notify_tree_changed() except AttributeError: pass def set_widget(self, index, widget): """ Sets the given widget at the index to replace the child currently at the position of that widget. """ w = self[index] self[index] = widget widget.parent = self widget.notify_added_to_parent(self) self.notify_removed_child(w) try: self.scene.notify_tree_changed() except AttributeError: pass def on_matrix_change(self): """ Notification of a changed matrix. """ pass def scene_matrix_reset(self): """ Resets the scene matrix. """ self.matrix.reset() self.on_matrix_change() def scene_post_scale(self, sx, sy=None, ax=0, ay=0): """ Adds a post_scale to the matrix. """ self.matrix.post_scale(sx, sy, ax, ay) self.on_matrix_change() def scene_post_pan(self, px, py): """ Adds a post_pan to the matrix. """ self.matrix.post_translate(px, py) self.on_matrix_change() def scene_post_rotate(self, angle, rx=0, ry=0): """ Adds a post_rotate to the matrix. """ self.matrix.post_rotate(angle, rx, ry) self.on_matrix_change() def scene_pre_scale(self, sx, sy=None, ax=0, ay=0): """ Adds a pre_scale to the matrix() """ self.matrix.pre_scale(sx, sy, ax, ay) self.on_matrix_change() def scene_pre_pan(self, px, py): """ Adds a pre_pan to the matrix() """ self.matrix.pre_translate(px, py) self.on_matrix_change() def scene_pre_rotate(self, angle, rx=0, ry=0): """ Adds a pre_rotate to the matrix() """ self.matrix.pre_rotate(angle, rx, ry) self.on_matrix_change() def get_scale_x(self): """ Gets the scale_x of the current matrix """ return self.matrix.value_scale_x() def get_scale_y(self): """ Gets the scale_y of the current matrix """ return self.matrix.value_scale_y() def get_skew_x(self): """ Gets the skew_x of the current matrix() """ return self.matrix.value_skew_x() def get_skew_y(self): """ Gets the skew_y of the current matrix() """ return self.matrix.value_skew_y() def get_translate_x(self): """ Gets the translate_x of the current matrix() """ return self.matrix.value_trans_x() def get_translate_y(self): """ Gets the translate_y of the current matrix() """ return self.matrix.value_trans_y()