def embroider(command, channel, _, angle=None, distance=None, **kwargs): elements = context.elements channel(_("Embroidery Filling")) efill = EulerianFill(float(distance)) for node in elements.elems(emphasized=True): try: path = Path(node.shape) except AttributeError: try: path = Path(node.path) except AttributeError: continue if angle is not None: path *= Matrix.rotate(angle) pts = [abs(path).point(i / 100.0, error=1e-4) for i in range(101)] efill += pts points = efill.get_fill() for s in split(points): result = Path(Polyline(s, stroke="black")) if angle is not None: result *= Matrix.rotate(-angle) node = elements.elem_branch.add(path=result, type="elem path") elements.classify([node])
def validate(self): if self.point is None: self.point = Point( float(self.settings.get("x", 0)), float(self.settings.get("y", 0)) ) if self.matrix is None: self.matrix = Matrix()
def eulerian_fill(settings, outlines, matrix, limit=None): """ Applies optimized Eulerian fill @return: """ if matrix is None: matrix = Matrix() settings = dict(settings) h_dist = settings.get("hatch_distance", "1mm") h_angle = settings.get("hatch_angle", "0deg") distance_y = float(Length(h_dist)) if isinstance(h_angle, float): angle = Angle.degrees(h_angle) else: angle = Angle.parse(h_angle) rotate = Matrix.rotate(angle) counter_rotate = Matrix.rotate(-angle) def mx_rotate(pt): if pt is None: return None return ( pt[0] * rotate.a + pt[1] * rotate.c + 1 * rotate.e, pt[0] * rotate.b + pt[1] * rotate.d + 1 * rotate.f, ) def mx_counter(pt): if pt is None: return None return ( pt[0] * counter_rotate.a + pt[1] * counter_rotate.c + 1 * counter_rotate.e, pt[0] * counter_rotate.b + pt[1] * counter_rotate.d + 1 * counter_rotate.f, ) transformed_vector = matrix.transform_vector([0, distance_y]) distance = abs(complex(transformed_vector[0], transformed_vector[1])) efill = EulerianFill(distance) for sp in outlines: sp = list(map(mx_rotate, sp)) efill += sp if limit and efill.estimate() > limit: return [] points = efill.get_fill() points = list(map(mx_counter, points)) return points
def test_fill_hatch2(self): kernel = bootstrap.bootstrap() try: kernel.console("operation* delete\n") kernel.console("rect 0 0 1in 1in\n") kernel.console("rect 3in 0 1in 1in\n") kernel.console("hatch\n") hatch = list(kernel.elements.ops())[0] hatch.hatch_type = "eulerian" rect0 = list(kernel.elements.elems())[0] hatch.add_node(copy(rect0)) rect1 = list(kernel.elements.elems())[1] hatch.add_node(copy(rect1)) commands = list() # kernel.console("tree list\n") hatch.preprocess(kernel.root, Matrix(), commands) for command in commands: command() # kernel.console("tree list\n") polyline_node0 = hatch.children[0] shape0 = polyline_node0.shape self.assertEqual(len(shape0), 77) # print(shape0) polyline_node1 = hatch.children[1] shape1 = polyline_node1.shape self.assertEqual(len(shape1), 50) # print(shape1) finally: kernel.shutdown()
def make_image(): step_x = self.raster_step_x step_y = self.raster_step_y bounds = self.bounds try: image = make_raster(list(self.flat()), bounds=bounds, step_x=step_x, step_y=step_y) except AssertionError: raise CutPlanningFailedError("Raster too large.") if image.width == 1 and image.height == 1: # TODO: Solve this is a less kludgy manner. The call to make the image can fail the first time # around because the renderer is what sets the size of the text. If the size hasn't already # been set, the initial bounds are wrong. bounds = self.bounds try: image = make_raster(list(self.flat()), bounds=bounds, step_x=step_x, step_y=step_y) except AssertionError: raise CutPlanningFailedError("Raster too large.") image = image.convert("L") matrix = Matrix.scale(step_x, step_y) matrix.post_translate(bounds[0], bounds[1]) image_node = ImageNode(image=image, matrix=matrix) self.children.clear() self.add_node(image_node)
def test_numpath_transform(self): numpath = Numpath() numpath.polyline( ( complex(0.05, 0.05), complex(0.95, 0.05), complex(0.95, 0.95), complex(0.05, 0.95), complex(0.05, 0.05), ) ) numpath.polyline( ( complex(0.25, 0.25), complex(0.75, 0.25), complex(0.75, 0.75), complex(0.25, 0.75), complex(0.25, 0.25), ) ) numpath.uscale(10000) c = copy(numpath) numpath.rotate(tau * 0.25) c.transform(Matrix("rotate(.25turn)")) t = numpath.segments == c.segments self.assertTrue(np.all(t))
def __init__( self, path=None, matrix=None, fill=None, stroke=None, stroke_width=None, linecap=Linecap.CAP_BUTT, linejoin=Linejoin.JOIN_MITER, fillrule=Fillrule.FILLRULE_NONZERO, *args, **kwargs, ): super().__init__(*args, **kwargs) self._formatter = "{element_type} {id} {stroke}" self.settings.update(kwargs) self.path = path if matrix is None: matrix = Matrix() self.matrix = matrix self.fill = fill self.stroke = stroke self.stroke_width = stroke_width self.linecap = linecap self.linejoin = linejoin self.fillrule = fillrule self.lock = False
def test_actualize_circle_step3_direct_black(self): """ Test for edge pixel error. Black Empty. :return: """ image = Image.new("RGBA", (256, 256), "black") draw = ImageDraw.Draw(image) draw.ellipse((100, 100, 150, 150), "white") for step in range(1, 20): transform = Matrix() actual, transform = actualize( image, transform, step_x=step, step_y=step, crop=False, inverted=True ) self.assertEqual(actual.getpixel((-1, -1)), 0) # Note: inverted flag not set. White edge pixel is correct. actual, transform = actualize(image, Matrix(), step_x=3, step_y=3, crop=False) self.assertEqual(actual.getpixel((-1, -1)), 255)
def test_plotplanner_walk_raster(self): """ Test plotplanner operation of walking to a raster. PLOT_FINISH = 256 PLOT_RAPID = 4 PLOT_JOG = 2 PLOT_SETTING = 128 PLOT_AXIS = 64 PLOT_DIRECTION = 32 PLOT_LEFT_UPPER = 512 PLOT_RIGHT_LOWER = 1024 1 means cut. 0 means move. :return: """ rasterop = RasterOpNode() image = Image.new("RGBA", (256, 256)) draw = ImageDraw.Draw(image) draw.ellipse((0, 0, 255, 255), "black") image = image.convert("L") inode = ImageNode(image=image, dpi=1000.0, matrix=Matrix()) inode.step_x = 1 inode.step_y = 1 inode.process_image() rasterop.add_node(inode) rasterop.raster_step_x = 1 rasterop.raster_step_y = 1 vectorop = EngraveOpNode() vectorop.add_node( PathNode(path=Path(Circle(cx=127, cy=127, r=128)), fill="black")) cutcode = CutCode() cutcode.extend(vectorop.as_cutobjects()) cutcode.extend(rasterop.as_cutobjects()) settings = {"power": 500} plan = PlotPlanner(settings) for c in cutcode.flat(): plan.push(c) setting_changed = False for x, y, on in plan.gen(): if on > 2: if setting_changed: # Settings change happens at vector to raster switch and must repost the axis. self.assertEqual(on, PLOT_AXIS) if on == PLOT_SETTING: setting_changed = True else: setting_changed = False
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 apply_rotary_scale(*args, **kwargs): sx = self.scale_x sy = self.scale_y x, y = self.device.current matrix = Matrix("scale(%f, %f, %f, %f)" % (sx, sy, x, y)) for node in self.elements.elems(): if hasattr(node, "rotary_scale"): # This element is already scaled return try: node.rotary_scale = sx, sy node.matrix *= matrix node.modified() except AttributeError: pass
def test_actualize_circle_step3_direct_white(self): """ Test for edge pixel error. White empty. :return: """ image = Image.new("RGBA", (256, 256), "white") draw = ImageDraw.Draw(image) draw.ellipse((100, 100, 150, 150), "black") for step in range(1, 20): transform = Matrix() actual, transform = actualize( image, transform, step_x=step, step_y=step, crop=False ) self.assertEqual(actual.getpixel((-1, -1)), 255)
def vectrace(data, **kwargs): elements = kernel.root.elements path = Path(fill="black", stroke="blue") paths = [] for node in data: matrix = node.matrix image = node.image width, height = node.image.size if image.mode != "L": image = image.convert("L") image = image.point(lambda e: int(e > 127) * 255) for points in _vectrace(image.load(), width, height): path += Polygon(*points) paths.append( elements.elem_branch.add(path=path, matrix=Matrix(matrix), type="elem path")) return "elements", paths
def linetext(command, channel, _, font=None, font_size=None, remainder=None, **kwargs): context.setting(str, "shx_preferred", None) if font is not None: context.shx_preferred = font font = context.shx_preferred safe_dir = realpath(get_safe_path(context.kernel.name)) if font is None: channel(_("SHX fonts in {path}:").format(path=safe_dir)) for p in glob(join(safe_dir, "*.shx")): channel(p) for p in glob(join(safe_dir, "*.SHX")): channel(p) return font_path = join(safe_dir, font) if not os.path.exists(font_path): channel( _("Font was not found at {path}").format(path=font_path)) for p in glob(join(safe_dir, "*.shx")): channel(p) for p in glob(join(safe_dir, "*.SHX")): channel(p) return if remainder is None: channel(_("No text to make a path with.")) return font = ShxFont(font_path) path = ShxPath() font.render(path, remainder, True, float(font_size)) path_node = PathNode( path=path.path, matrix=Matrix.translate(0, float(font_size)), stroke=Color("black"), ) context.elements.elem_branch.add_node(path_node) context.signal("element_added", path_node)
def event(self, window_pos=None, space_pos=None, event_type=None, nearest_snap=None): response = RESPONSE_CHAIN if event_type == "leftclick": if nearest_snap is None: point = Point(space_pos[0], space_pos[1]) else: point = Point(nearest_snap[0], nearest_snap[1]) elements = self.scene.context.elements node = elements.elem_branch.add(point=point, matrix=Matrix(), type="elem point") if self.scene.context.elements.default_stroke is not None: node.stroke = self.scene.context.elements.default_stroke if self.scene.context.elements.default_fill is not None: node.fill = self.scene.context.elements.default_fill if elements.classify_new: elements.classify([node]) self.notify_created(node) response = RESPONSE_CONSUME return response
def test_fill_euler_scale(self): w = 1000 h = 1000 paths = ( ( (w * 0.05, h * 0.05), (w * 0.95, h * 0.05), (w * 0.95, h * 0.95), (w * 0.05, h * 0.95), (w * 0.05, h * 0.05), ), ( (w * 0.25, h * 0.25), (w * 0.75, h * 0.25), (w * 0.75, h * 0.75), (w * 0.25, h * 0.75), (w * 0.25, h * 0.25), ), ) matrix = Matrix.scale(0.005) fill = list(eulerian_fill(settings={}, outlines=paths, matrix=matrix)) self.assertEqual(len(fill), 327) for x, y in fill: self.assertIn(x, (50, 250, 750, 950))
def test_actualize_pureblack(self): """ Test that a pure black image does not crash. :return: """ kernel = bootstrap.bootstrap() try: kernel_root = kernel.get_context("/") # kernel_root("channel print console\n") image = Image.new("RGBA", (256, 256), "black") elements = kernel_root.elements node = elements.elem_branch.add( image=image, dpi=1000.0, matrix=Matrix(), type="elem image" ) node.emphasized = True kernel_root("image resample\n") for element in kernel_root.elements.elems(): if node.type == "elem image": self.assertEqual(element.image.size, (256, 256)) self.assertEqual(element.matrix.value_trans_x(), 0) self.assertEqual(element.matrix.value_trans_y(), 0) finally: kernel.shutdown()
def rebuild_hit_chain(self, current_widget, current_matrix=None): """ Iterates through the hit chain to find elements which respond to their hit() function that they are HITCHAIN_HIT and registers this within the hittable_elements list if they are able to hit at the current time. Given the dimensions of the widget and the current matrix within the widget tree. HITCHAIN_HIT means that this is a hit value and should the termination of this branch of the widget tree. HITCHAIN_DELEGATE means that this is not a hittable widget and should not receive mouse events. HITCHAIN_HIT_AND_DELEGATE means that this is a hittable widget, but other widgets within it might also matter. HITCHAIN_DELEGATE_AND_HIT means that other widgets in the tree should be checked first, but after those this widget should be checked. The hitchain is the current matrix and current widget in the order of depth. """ # If there is a matrix for the widget concatenate it. if current_widget.matrix is not None: matrix_within_scene = Matrix(current_widget.matrix) matrix_within_scene.post_cat(current_matrix) else: matrix_within_scene = Matrix(current_matrix) # Add to list and recurse for children based on response. response = current_widget.hit() if response == HITCHAIN_HIT: self.hittable_elements.append( (current_widget, matrix_within_scene)) # elif response == HITCHAIN_HIT_WITH_PRIORITY: # self.hittable_elements.insert(0, (current_widget, matrix_within_scene)) elif response == HITCHAIN_DELEGATE: for w in current_widget: self.rebuild_hit_chain(w, matrix_within_scene) elif response == HITCHAIN_HIT_AND_DELEGATE: self.hittable_elements.append( (current_widget, matrix_within_scene)) for w in current_widget: self.rebuild_hit_chain(w, matrix_within_scene) elif response == HITCHAIN_DELEGATE_AND_HIT: for w in current_widget: self.rebuild_hit_chain(w, matrix_within_scene) self.hittable_elements.append( (current_widget, matrix_within_scene))
def scanline_fill(settings, outlines, matrix, limit=None): """ Applies optimized scanline fill @return: """ if matrix is None: matrix = Matrix() settings = dict(settings) h_dist = settings.get("hatch_distance", "1mm") h_angle = settings.get("hatch_angle", "0deg") distance_y = float(Length(h_dist)) if isinstance(h_angle, float): angle = Angle.degrees(h_angle) else: angle = Angle.parse(h_angle) rotate = Matrix.rotate(angle) counter_rotate = Matrix.rotate(-angle) def mx_rotate(pt): if pt is None: return None return ( pt[0] * rotate.a + pt[1] * rotate.c + 1 * rotate.e, pt[0] * rotate.b + pt[1] * rotate.d + 1 * rotate.f, ) def mx_counter(pt): if pt is None: return None return ( pt[0] * counter_rotate.a + pt[1] * counter_rotate.c + 1 * counter_rotate.e, pt[0] * counter_rotate.b + pt[1] * counter_rotate.d + 1 * counter_rotate.f, ) transformed_vector = matrix.transform_vector([0, distance_y]) distance = abs(complex(transformed_vector[0], transformed_vector[1])) vm = VectorMontonizer() for outline in outlines: pts = list(map(Point, map(mx_rotate, outline))) vm.add_cluster(pts) vm.sort_clusters() y_max = vm.clusters[-1][0] y_min = vm.clusters[0][0] height = y_max - y_min try: count = height / distance except ZeroDivisionError: return [] if limit and count > limit: return [] vm.valid_low_value = y_min - distance vm.valid_high_value = y_max + distance vm.scanline(y_min - distance) points = list() forward = True while vm.valid_range(): vm.next_intercept(distance) vm.sort_actives() y = vm.current for i in ( range(1, len(vm.actives), 2) if forward else range(len(vm.actives) - 1, 0, -2) ): left_segment = vm.actives[i - 1] right_segment = vm.actives[i] left_segment_x = vm.intercept(left_segment, y) right_segment_x = vm.intercept(right_segment, y) if forward: points.append((left_segment_x, y)) points.append((right_segment_x, y)) else: points.append((right_segment_x, y)) points.append((left_segment_x, y)) points.append(None) forward = not forward points = list(map(mx_counter, points)) return points
def draw_cutcode(self, cutcode: CutCode, gc: wx.GraphicsContext, x: int = 0, y: int = 0): """ Draw cutcode object into wxPython graphics code. This code accepts x,y offset values. The cutcode laser offset can be set with a command with the rest of the cutcode remaining the same. So drawing the cutcode requires knowing what, if any offset is currently being applied. @param cutcode: flat cutcode object to draw. @param gc: wx.graphics context @param x: offset in x direction @param y: offset in y direction @return: """ p = None last_point = None color = None for cut in cutcode: c = cut.line_color if c is not color: color = c last_point = None if p is not None: gc.StrokePath(p) del p p = gc.CreatePath() self.set_pen(gc, c, width=7.0, alpha=127) start = cut.start end = cut.end if p is None: p = gc.CreatePath() if last_point != start: p.MoveToPoint(start[0] + x, start[1] + y) if isinstance(cut, LineCut): # Standard line cut. Applies to path object. p.AddLineToPoint(end[0] + x, end[1] + y) elif isinstance(cut, QuadCut): # Standard quadratic bezier cut p.AddQuadCurveToPoint(cut.c()[0] + x, cut.c()[1] + y, end[0] + x, end[1] + y) elif isinstance(cut, CubicCut): # Standard cubic bezier cut p.AddCurveToPoint( cut.c1()[0] + x, cut.c1()[1] + y, cut.c2()[0] + x, cut.c2()[1] + y, end[0] + x, end[1] + y, ) elif isinstance(cut, RasterCut): # Rastercut object. image = cut.image gc.PushState() matrix = Matrix.scale(cut.step_x, cut.step_y) matrix.post_translate(cut.offset_x + x, cut.offset_y + y) # Adjust image xy gc.ConcatTransform( wx.GraphicsContext.CreateMatrix(gc, ZMatrix(matrix))) try: cache = cut.cache cache_id = cut.cache_id except AttributeError: cache = None cache_id = -1 if cache_id != id(image): # Cached image is invalid. cache = None if cache is None: # No valid cache. Generate. cut.c_width, cut.c_height = image.size try: cut.cache = self.make_thumbnail(image, maximum=5000) except (MemoryError, RuntimeError): cut.cache = None cut.cache_id = id(image) if cut.cache is not None: # Cache exists and is valid. gc.DrawBitmap(cut.cache, 0, 0, cut.c_width, cut.c_height) else: # Image was too large to cache, draw a red rectangle instead. gc.SetBrush(wx.RED_BRUSH) gc.DrawRectangle(0, 0, cut.c_width, cut.c_height) gc.DrawBitmap(icons8_image_50.GetBitmap(), 0, 0, cut.c_width, cut.c_height) gc.PopState() elif isinstance(cut, RawCut): pass elif isinstance(cut, PlotCut): p.MoveToPoint(start[0] + x, start[1] + y) for px, py, pon in cut.plot: if pon == 0: p.MoveToPoint(px + x, py + y) else: p.AddLineToPoint(px + x, py + y) elif isinstance(cut, DwellCut): pass elif isinstance(cut, WaitCut): pass elif isinstance(cut, InputCut): pass elif isinstance(cut, OutputCut): pass last_point = end if p is not None: gc.StrokePath(p) del p
class ViewPort: """ The width and height are of the viewport are stored in MK native units (nm). Origin_x and origin_y are the location of the home position in unit square values. This is to say 1,1 is the bottom left, and 0.5 0.5 is the middle of the bed. user_scale is a scale factor for applied by the user rather than the driver. native_scale is the scale factor of the driver units to MK native units flip_x, flip_y, and swap_xy are used to apply whatever flips and swaps are needed. """ def __init__( self, width, height, origin_x=0.0, origin_y=0.0, user_scale_x=1.0, user_scale_y=1.0, native_scale_x=1.0, native_scale_y=1.0, flip_x=False, flip_y=False, swap_xy=False, show_origin_x=None, show_origin_y=None, ): self._matrix = None self._imatrix = None self.width = width self.height = height self.origin_x = origin_x self.origin_y = origin_y self.user_scale_x = user_scale_x self.user_scale_y = user_scale_y self.native_scale_x = native_scale_x self.native_scale_y = native_scale_y self.flip_x = flip_x self.flip_y = flip_y self.swap_xy = swap_xy if show_origin_x is None: show_origin_x = origin_x if show_origin_y is None: show_origin_y = origin_y self.show_origin_x = show_origin_x self.show_origin_y = show_origin_y self._width = None self._height = None self._offset_x = None self._offset_y = None self._scale_x = None self._scale_y = None self.realize() def realize(self): self._imatrix = None self._matrix = None self._width = Length(self.width, unitless=1.0).units self._height = Length(self.height, unitless=1.0).units self._offset_x = self._width * self.origin_x self._offset_y = self._height * self.origin_y self._scale_x = self.user_scale_x * self.native_scale_x self._scale_y = self.user_scale_y * self.native_scale_y def physical_to_scene_position(self, x, y, unitless=UNITS_PER_PIXEL): """ Converts an X,Y position into viewport units. @param x: @param y: @param as_float: @param unitless: @return: """ unit_x = Length(x, relative_length=self._width, unitless=unitless).units unit_y = Length(y, relative_length=self._height, unitless=unitless).units return unit_x, unit_y def physical_to_device_position(self, x, y, unitless=UNITS_PER_PIXEL): """ Converts an X,Y position into viewport units. @param x: @param y: @param unitless: @return: """ x, y = self.physical_to_scene_position(x, y, unitless) return self.scene_to_device_position(x, y) def physical_to_device_length(self, x, y, unitless=UNITS_PER_PIXEL): """ Converts an X,Y position into dx, dy. @param x: @param y: @param unitless: @return: """ x, y = self.physical_to_scene_position(x, y, unitless) return self.scene_to_device_position(x, y, vector=True) def device_to_scene_position(self, x, y): if self._imatrix is None: self.calculate_matrices() point = self._imatrix.point_in_matrix_space((x, y)) return point.x, point.y def scene_to_device_position(self, x, y, vector=False): if self._matrix is None: self.calculate_matrices() if vector: point = self._matrix.transform_vector([x, y]) return point[0], point[1] else: point = self._matrix.point_in_matrix_space((x, y)) return point.x, point.y def calculate_matrices(self): self._matrix = Matrix(self.scene_to_device_matrix()) self._imatrix = Matrix(self._matrix) self._imatrix.inverse() def device_to_scene_matrix(self): if self._matrix is None: self.calculate_matrices() return self._imatrix def scene_to_device_matrix(self): ops = [] if self._scale_x != 1.0 or self._scale_y != 1.0: ops.append("scale({sx:.13f}, {sy:.13f})".format( sx=1.0 / self._scale_x, sy=1.0 / self._scale_y)) if self._offset_x != 0 or self._offset_y != 0: ops.append("translate({dx:.13f}, {dy:.13f})".format( dx=self._offset_x, dy=self._offset_y)) if self.flip_y: ops.append("scale(1.0, -1.0)") if self.flip_x: ops.append("scale(-1.0, 1.0)") if self.swap_xy: ops.append("scale(-1.0, 1.0) rotate(90deg)") return " ".join(ops) def length( self, value, axis=None, new_units=None, relative_length=None, as_float=False, unitless=UNITS_PER_PIXEL, digits=None, scale=None, ): """ Axis 0 is X Axis 1 is Y Axis -1 is 1D in x, y space. e.g. a line width. Convert a length of distance {value} to new native values. @param value: @param axis: @param new_units: @param relative_length: @param as_float: @param unitless: factor for units with no units sets. @param scale: scale length by given factor. @return: """ if axis == 0: if relative_length is None: relative_length = self.width else: if relative_length is None: relative_length = self.height length = Length(value, relative_length=relative_length, unitless=unitless, digits=digits) if scale is not None: length *= scale if new_units == "mm": if as_float: return length.mm else: return length.length_mm elif new_units == "inch": if as_float: return length.inches else: return length.length_inches elif new_units == "cm": if as_float: return length.cm else: return length.length_cm elif new_units == "px": if as_float: return length.pixels else: return length.length_pixels elif new_units == "mil": if as_float: return length.mil else: return length.length_mil else: return length.units def contains(self, x, y): x = self.length(x, 0) y = self.length(y, 1) if x >= self._width: return False if y >= self._height: return False if x < 0: return False if y < 0: return False return True def bbox(self): return ( 0, 0, self.unit_width, self.unit_height, ) def dpi_to_steps(self, dpi, matrix=None): """ Converts a DPI to a given step amount within the device length values. So M2 Nano will have 1 step per mil, the DPI of 500 therefore is step_x 2, step_y 2. A Galvo laser with a 200mm lens will have steps equal to 200mm/65536 ~= 0.12 mils. So a DPI of 500 needs a step size of ~16.65 for x and y. Since 500 DPI is one dot per 2 mils. Note, steps size can be negative if our driver is x or y flipped. @param dpi: @return: """ # We require vectors so any positional offsets are non-contributing. unit_x = UNITS_PER_INCH unit_y = UNITS_PER_INCH if matrix is None: if self._matrix is None: self.calculate_matrices() matrix = self._matrix oneinch_x = matrix.transform_vector([unit_x, 0])[0] oneinch_y = matrix.transform_vector([0, unit_y])[1] step_x = float(oneinch_x / dpi) step_y = float(oneinch_y / dpi) return step_x, step_y @property def length_width(self): return Length(self.width) @property def length_height(self): return Length(self.height) @property def unit_width(self): return float(Length(self.width)) @property def unit_height(self): return float(Length(self.height)) @staticmethod def viewbox_transform(e_x, e_y, e_width, e_height, vb_x, vb_y, vb_width, vb_height, aspect): """ SVG 1.1 7.2, SVG 2.0 8.2 equivalent transform of an SVG viewport. With regards to https://github.com/w3c/svgwg/issues/215 use 8.2 version. It creates transform commands equal to that viewport expected. Let e-x, e-y, e-width, e-height be the position and size of the element respectively. Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y, width and height values of the viewBox attribute respectively. Let align be align value of preserveAspectRatio, or 'xMidYMid' if preserveAspectRatio is not defined. Let meetOrSlice be the meetOrSlice value of preserveAspectRatio, or 'meet' if preserveAspectRatio is not defined or if meetOrSlice is missing from this value. @param e_x: element_x value @param e_y: element_y value @param e_width: element_width value @param e_height: element_height value @param vb_x: viewbox_x value @param vb_y: viewbox_y value @param vb_width: viewbox_width value @param vb_height: viewbox_height value @param aspect: preserve aspect ratio value @return: string of the SVG transform commands to account for the viewbox. """ if (e_x is None or e_y is None or e_width is None or e_height is None or vb_x is None or vb_y is None or vb_width is None or vb_height is None): return "" if aspect is not None: aspect_slice = aspect.split(" ") try: align = aspect_slice[0] except IndexError: align = "xMidyMid" try: meet_or_slice = aspect_slice[1] except IndexError: meet_or_slice = "meet" else: align = "xMidyMid" meet_or_slice = "meet" # Initialize scale-x to e-width/vb-width. scale_x = e_width / vb_width # Initialize scale-y to e-height/vb-height. scale_y = e_height / vb_height # If align is not 'none' and meetOrSlice is 'meet', set the larger of scale-x and scale-y to the smaller. if align != "none" and meet_or_slice == "meet": scale_x = scale_y = min(scale_x, scale_y) # Otherwise, if align is not 'none' and meetOrSlice is 'slice', set the smaller of scale-x and scale-y to the larger elif align != "none" and meet_or_slice == "slice": scale_x = scale_y = max(scale_x, scale_y) # Initialize translate-x to e-x - (vb-x * scale-x). translate_x = e_x - (vb_x * scale_x) # Initialize translate-y to e-y - (vb-y * scale-y) translate_y = e_y - (vb_y * scale_y) # If align contains 'xMid', add (e-width - vb-width * scale-x) / 2 to translate-x. align = align.lower() if "xmid" in align: translate_x += (e_width - vb_width * scale_x) / 2.0 # If align contains 'xMax', add (e-width - vb-width * scale-x) to translate-x. if "xmax" in align: translate_x += e_width - vb_width * scale_x # If align contains 'yMid', add (e-height - vb-height * scale-y) / 2 to translate-y. if "ymid" in align: translate_y += (e_height - vb_height * scale_y) / 2.0 # If align contains 'yMax', add (e-height - vb-height * scale-y) to translate-y. if "ymax" in align: translate_y += e_height - vb_height * scale_y # The transform applied to content contained by the element is given by: # translate(translate-x, translate-y) scale(scale-x, scale-y) if isinstance(scale_x, Length) or isinstance(scale_y, Length): raise ValueError if translate_x == 0 and translate_y == 0: if scale_x == 1 and scale_y == 1: return "" # Nothing happens. else: return "scale(%s, %s)" % (Length.str(scale_x), Length.str(scale_y)) else: if scale_x == 1 and scale_y == 1: return "translate(%s, %s)" % ( Length.str(translate_x), Length.str(translate_y), ) else: return "translate(%s, %s) scale(%s, %s)" % ( Length.str(translate_x), Length.str(translate_y), Length.str(scale_x), Length.str(scale_y), ) @staticmethod def conversion(units, amount=1): return Length("{amount}{units}".format(units=units, amount=amount)).preferred
def calculate_matrices(self): self._matrix = Matrix(self.scene_to_device_matrix()) self._imatrix = Matrix(self._matrix) self._imatrix.inverse()
def aspect_matrix(self): """ Specifically view the scene with the given Viewbox. """ if self._frame and self._view and self.aspect: self.scene_widget.matrix = Matrix(self._view.transform(self._frame))
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 PointNode(Node): """ PointNode is the bootstrapped node type for the 'elem path' type. """ def __init__( self, point=None, matrix=None, fill=None, stroke=None, stroke_width=None, **kwargs, ): super(PointNode, self).__init__(type="elem point", **kwargs) self._formatter = "{element_type} {id} {stroke}" self.point = point self.matrix = matrix self.settings = kwargs self.fill = fill self.stroke = stroke self.stroke_width = stroke_width self.lock = False def __copy__(self): return PointNode( point=copy(self.point), matrix=copy(self.matrix), fill=copy(self.fill), stroke=copy(self.stroke), stroke_width=self.stroke_width, **self.settings, ) def validate(self): if self.point is None: self.point = Point( float(self.settings.get("x", 0)), float(self.settings.get("y", 0)) ) if self.matrix is None: self.matrix = Matrix() def preprocess(self, context, matrix, commands): self.matrix *= matrix self._bounds_dirty = True @property def bounds(self): if self._bounds_dirty: p = self.matrix.transform_point(self.point) self._bounds = ( p[0], p[1], p[0], p[1], ) return self._bounds def default_map(self, default_map=None): default_map = super(PointNode, self).default_map(default_map=default_map) default_map["element_type"] = "Point" if self.point is not None: default_map["x"] = self.point[0] default_map["y"] = self.point[1] else: default_map["x"] = 0 default_map["y"] = 0 default_map.update(self.settings) default_map["stroke"] = self.stroke default_map["fill"] = self.fill default_map["stroke-width"] = self.stroke_width default_map["matrix"] = self.matrix 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"] self._points.append([self.point.x, self.point.y, "point"]) def update_point(self, index, point): return False def add_point(self, point, index=None): return False
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
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
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))))
def test_cutcode_image(self): """ Convert CutCode from Image operation Test image-based crosshatched setting :return: """ laserop = ImageOpNode() # Add Path initial = "M 0,0 L 100,100 L 0,0 M 50,-50 L 100,-100 M 0,0 Q 100,100 200,0" path = Path(initial) laserop.add_node(PathNode(path)) # Add SVG Image1 image = Image.new("RGBA", (256, 256), (255, 255, 255, 0)) draw = ImageDraw.Draw(image) draw.ellipse((50, 50, 150, 150), "white") draw.ellipse((100, 100, 105, 105), "black") inode1 = ImageNode(image=image, matrix=Matrix(), dpi=1000.0 / 3.0) inode1.step_x = 3 inode1.step_y = 3 inode1.process_image() laserop.add_node(inode1) # Add SVG Image2 image2 = Image.new("RGBA", (256, 256), (255, 255, 255, 0)) draw = ImageDraw.Draw(image2) draw.ellipse((50, 50, 150, 150), "white") draw.ellipse((80, 80, 120, 120), "black") inode2 = ImageNode(image=image2, matrix=Matrix(), dpi=500, direction=4) inode2.step_x = 2 inode2.step_y = 2 inode2.process_image() laserop.add_node(inode2) # crosshatch for i in range(2): # Check for knockon cutcode = CutCode(laserop.as_cutobjects()) self.assertEqual(len(cutcode), 3) rastercut = cutcode[0] self.assertTrue(isinstance(rastercut, RasterCut)) self.assertEqual(rastercut.offset_x, 100) self.assertEqual(rastercut.offset_y, 100) image = rastercut.image self.assertTrue(isinstance(image, Image.Image)) self.assertIn(image.mode, ("L", "1")) self.assertEqual(image.size, (2, 2)) # step value 2, 6/2 self.assertEqual( rastercut.path, "M 100,100 L 100,106 L 106,106 L 106,100 Z" ) rastercut1 = cutcode[1] self.assertTrue(isinstance(rastercut1, RasterCut)) self.assertEqual(rastercut1.offset_x, 80) self.assertEqual(rastercut1.offset_y, 80) image1 = rastercut1.image self.assertTrue(isinstance(image1, Image.Image)) self.assertIn(image1.mode, ("L", "1")) self.assertEqual(image1.size, (21, 21)) # default step value 2, 40/2 + 1 self.assertEqual(rastercut1.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z") rastercut2 = cutcode[2] self.assertTrue(isinstance(rastercut2, RasterCut)) self.assertEqual(rastercut2.offset_x, 80) self.assertEqual(rastercut2.offset_y, 80) image2 = rastercut2.image self.assertTrue(isinstance(image2, Image.Image)) self.assertIn(image2.mode, ("L", "1")) self.assertEqual(image2.size, (21, 21)) # default step value 2, 40/2 + 1 self.assertEqual(rastercut2.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")
def test_cutcode_image_crosshatch(self): """ Convert CutCode from Image Operation. Test ImageOp Crosshatch Setting :return: """ laserop = ImageOpNode(raster_direction=4) # Add Path initial = "M 0,0 L 100,100 L 0,0 M 50,-50 L 100,-100 M 0,0 Q 100,100 200,0" path = Path(initial) laserop.add_node(PathNode(path)) # Add SVG Image1 image1 = Image.new("RGBA", (256, 256), (255, 255, 255, 0)) draw = ImageDraw.Draw(image1) draw.ellipse((50, 50, 150, 150), "white") draw.ellipse((100, 100, 105, 105), "black") inode = ImageNode(image=image1, matrix=Matrix(), dpi=1000.0 / 3.0) inode.step_x = 3 inode.step_y = 3 inode.process_image() laserop.add_node(inode) # Add SVG Image2 image2 = Image.new("RGBA", (256, 256), (255, 255, 255, 0)) draw = ImageDraw.Draw(image2) draw.ellipse((50, 50, 150, 150), "white") draw.ellipse((80, 80, 120, 120), "black") inode = ImageNode(image=image2, matrix=Matrix(), dpi=500.0) inode.step_x = 2 inode.step_y = 2 inode.process_image() laserop.add_node(inode) # Add SVG Image3 inode = ImageNode(image=image2, matrix=Matrix(), dpi=1000.0 / 3.0) inode.step_x = 3 inode.step_y = 3 inode.process_image() laserop.add_node(inode) cutcode = CutCode(laserop.as_cutobjects()) self.assertEqual(len(cutcode), 6) rastercut1_0 = cutcode[0] self.assertTrue(isinstance(rastercut1_0, RasterCut)) self.assertEqual(rastercut1_0.offset_x, 100) self.assertEqual(rastercut1_0.offset_y, 100) image = rastercut1_0.image self.assertTrue(isinstance(image, Image.Image)) self.assertIn(image.mode, ("L", "1")) self.assertEqual(image.size, (2, 2)) # step value 2, 6/2 self.assertEqual(rastercut1_0.path, "M 100,100 L 100,106 L 106,106 L 106,100 Z") rastercut1_1 = cutcode[1] self.assertTrue(isinstance(rastercut1_1, RasterCut)) self.assertEqual(rastercut1_1.offset_x, 100) self.assertEqual(rastercut1_1.offset_y, 100) image = rastercut1_1.image self.assertTrue(isinstance(image, Image.Image)) self.assertIn(image.mode, ("L", "1")) self.assertEqual(image.size, (2, 2)) # step value 2, 6/2 self.assertEqual(rastercut1_1.path, "M 100,100 L 100,106 L 106,106 L 106,100 Z") rastercut2_0 = cutcode[2] self.assertTrue(isinstance(rastercut2_0, RasterCut)) self.assertEqual(rastercut2_0.offset_x, 80) self.assertEqual(rastercut2_0.offset_y, 80) image1 = rastercut2_0.image self.assertTrue(isinstance(image1, Image.Image)) self.assertIn(image1.mode, ("L", "1")) self.assertEqual(image1.size, (21, 21)) # default step value 2, 40/2 + 1 self.assertEqual(rastercut2_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z") rastercut2_1 = cutcode[3] self.assertTrue(isinstance(rastercut2_1, RasterCut)) self.assertEqual(rastercut2_1.offset_x, 80) self.assertEqual(rastercut2_1.offset_y, 80) image2 = rastercut2_1.image self.assertTrue(isinstance(image2, Image.Image)) self.assertIn(image2.mode, ("L", "1")) self.assertEqual(image2.size, (21, 21)) # default step value 2, 40/2 + 1 self.assertEqual(rastercut2_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z") rastercut3_0 = cutcode[4] self.assertTrue(isinstance(rastercut3_0, RasterCut)) self.assertEqual(rastercut3_0.offset_x, 80) self.assertEqual(rastercut3_0.offset_y, 80) image3 = rastercut3_0.image self.assertTrue(isinstance(image3, Image.Image)) self.assertIn(image3.mode, ("L", "1")) self.assertEqual(image3.size, (14, 14)) # default step value 3, ceil(40/3) + 1 self.assertEqual(rastercut3_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z") rastercut3_1 = cutcode[5] self.assertTrue(isinstance(rastercut3_1, RasterCut)) self.assertEqual(rastercut3_1.offset_x, 80) self.assertEqual(rastercut3_1.offset_y, 80) image4 = rastercut3_1.image self.assertTrue(isinstance(image4, Image.Image)) self.assertIn(image4.mode, ("L", "1")) self.assertEqual(image4.size, (14, 14)) # default step value 3, ceil(40/3) + 1 self.assertEqual(rastercut2_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")