def add_grid(obj: svgwrite.Drawing, s=10): """ function to add grid to SVG drawing, must be called after set_bg_color if used :param obj: svgwrite.Drawing object :param s: grid step :return: None """ pattern_small_grid = obj.pattern(insert=None, size=(s, s), patternUnits='userSpaceOnUse') pattern_small_grid.attribs['id'] = "smallGrid" path_small = obj.path(f'M {s} 0 L 0 0 0 {s}', stroke='gray', fill='none', style=f'stroke-width:{0.5}') pattern_small_grid.add(path_small) pattern_large_grid = obj.pattern(insert=None, size=(s * 10, s * 10), patternUnits='userSpaceOnUse') pattern_large_grid.attribs['id'] = "grid" path_large = obj.path(f'M {s * 10} 0 L 0 0 0 {s * 10}', stroke='gray', fill='none', style=f'stroke-width:{1}') pattern_large_grid.add(path_large) rect_large = obj.rect(fill="url(#smallGrid)", size=(s * 10, s * 10)) pattern_large_grid.add(rect_large) obj.defs.add(pattern_small_grid) obj.defs.add(pattern_large_grid) rect_grid = obj.rect(size=('100%', '100%'), fill="url(#grid)") obj.add(rect_grid)
def render(self, dwg: Drawing) -> Group: g = dwg.g() leg = dwg.path(fill=self.leg_color) leg.push("M 0 0") leg.push("L 0 %f" % self.leg_length) leg.push("l %f 0" % self.thickness_foot) leg.push("L %f 0" % self.thickness_thigh) leg.push("Z") g.add(leg) boot_start = .7 boot_height = self.boot_height foot_length = self.foot_length boot = dwg.path(fill=self.foot_color) boot.push("M 0 %f" % (self.leg_length * boot_start)) boot.push("L 0 %f" % (self.leg_length + boot_height)) boot.push("l %f 0" % foot_length) boot.push( "a %f %f 0 0 0 %f %f" % (min(boot_height, abs(foot_length - self.thickness_foot)), boot_height, -min(boot_height, foot_length - self.thickness_foot), -boot_height)) boot.push("L %f %f" % (self.thickness_thigh - (self.thickness_thigh - self.thickness_foot) * boot_start + 1, self.leg_length * boot_start)) g.add(boot) return g
def create_path(legendspec, curve, dwg: svgwrite.Drawing, s): path_base = dwg.path(d=curve) path_base.fill("none").stroke(legendspec[0], width=6 if not s else 3) paths = [path_base] if legendspec[1]: path_dashes = dwg.path(d=curve) path_dashes.fill("none").stroke(legendspec[2][1], width=6 if not s else 3) path_dashes.dasharray(legendspec[2][0]) paths.append(path_dashes) for i in paths: dwg.add(i) return paths
class Writer(): def __init__(self, fontfn, outfn, size, pad=0, sw=0.2, nl=10): self.pos = (pad, pad) self.sw = sw self.pad = pad self.nl = nl self.xdst = 1 self.dwg = Drawing(str(outfn), size=size, profile='tiny', debug=False) with open(str(fontfn), 'r') as f: self.symbols = load(f)['symbols'] def newline(self): self.pos = (self.pad, self.pos[1] + self.nl) def write(self, phrase): for s in phrase: if s in self.symbols: o = self.symbols[s] gw = o['w'] paths = o['paths'] for path in paths: self.dwg.add( self.dwg.path(d=tosvgpath( list(shift_path(path, self.pos))), stroke=black, fill='none', stroke_width=self.sw)) self.pos = _rel_move(self.pos, (gw + self.xdst, 0)) else: print('symbol not found: {:s}'.format(s)) self.dwg.save(pretty=True, indent=2)
def test_jump_reduction(): paths = [] rect_width = 100 rect_height = rect_width / 2 for i in range(3): y_offset = rect_width*i*1j corners = [rect_height, rect_width+rect_height, rect_width+rect_height + rect_height*1j, rect_height*1j+ rect_height] corners = [c+y_offset for c in corners] lines = [Line(start=corners[j], end=corners[(j+1) % len(corners)]) for j in range(len(corners))] _path = Path(*lines) _path = _path.rotated(i*20) paths += list(_path) max_y = max([p.start.imag for p in paths]+[p.end.imag for p in paths]) max_x = max([p.start.real for p in paths]+[p.end.real for p in paths]) filename = "test_jump_reduction.svg" viewbox = [0, -rect_height, max_x+2*rect_height, max_y+2*rect_height] dwg = Drawing(filename, width="10cm", viewBox=" ".join([str(b) for b in viewbox])) dwg.add(dwg.path(d=Path(*paths).d())) dwg.save() dig = Digitizer() dig.filecontents = open(filename, "r").read() dig.svg_to_pattern() pattern_to_svg(dig.pattern, join(filename + ".svg"))
def save_pie_chart(filename, all_angles, step_size, colors): # create the drawing surface svg_drawing = Drawing(filename=filename, size=(SVG_SIZE, SVG_SIZE), debug=True) start_x = SVG_SIZE // 2 start_y = SVG_SIZE // 2 radius = SVG_SIZE // 2 radians0 = all_angles[-1] for i in range(len(all_angles)): radians1 = all_angles[i] dx0 = radius * (math.sin(radians0)) dy0 = radius * (math.cos(radians0)) dx1 = radius * (math.sin(radians1)) dy1 = radius * (math.cos(radians1)) m0 = round(dy0, 9) n0 = round(-dx0, 9) m1 = round(-dy0 + dy1, 9) n1 = round(dx0 - dx1, 9) w = svg_drawing.path( d="M {0},{1} l {2},{3} a {4},{4} 0 0,0 {5},{6} z".format( start_x, start_y, m0, n0, radius, m1, n1), fill=colors[i], stroke="none", ) svg_drawing.add(w) radians0 = radians1 svg_drawing.save()
def _draw_circle_segment( self, dr: svgwrite.Drawing, tracks: List[Track], a1: float, a2: float, rr: ValueRange, center: XY, ): length = sum([t.length for t in tracks]) has_special = len([t for t in tracks if t.special]) > 0 color = self.color(self.poster.length_range_by_date, length, has_special) r1 = rr.lower() r2 = ( rr.lower() + rr.diameter() * length / self.poster.length_range_by_date.upper()) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a2, cos_a2 = math.sin(a2), math.cos(a2) path = dr.path( d=("M", center.x + r1 * sin_a1, center.y - r1 * cos_a1), fill=color, stroke="none", ) path.push("l", (r2 - r1) * sin_a1, (r1 - r2) * cos_a1) path.push( f"a{r2},{r2} 0 0,0 {r2 * (sin_a2 - sin_a1)},{r2 * (cos_a1 - cos_a2)}" ) path.push("l", (r1 - r2) * sin_a2, (r2 - r1) * cos_a2) dr.add(path)
def _line_to_path(dwg: svgwrite.Drawing, lines: Union[np.ndarray, LineCollection]): """Convert a line into a SVG path element. Accepts either a single line or a :py:class:`LineCollection`. Args: lines: line(s) to convert to path Returns: (svgwrite element): path element """ if isinstance(lines, np.ndarray): lines = [lines] def single_line_to_path(line: np.ndarray) -> str: if line[0] == line[-1]: closed = True line = line[:-1] else: closed = False return ("M" + " L".join(f"{x},{y}" for x, y in as_vector(line)) + (" Z" if closed else "")) return dwg.path(" ".join(single_line_to_path(line) for line in lines))
def _draw_circle_segment( self, dr: svgwrite.Drawing, g: svgwrite.container.Group, tracks: typing.List[Track], a1: float, a2: float, rr: ValueRange, center: XY, ) -> None: length = sum([t.length() for t in tracks]) has_special = len([t for t in tracks if t.special]) > 0 color = self.color(self.poster.length_range_by_date, length, has_special) max_length = self.poster.length_range_by_date.upper() assert max_length is not None r1 = rr.lower() assert r1 is not None r2 = rr.interpolate((length / max_length).magnitude) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a2, cos_a2 = math.sin(a2), math.cos(a2) path = dr.path( d=("M", center.x + r1 * sin_a1, center.y - r1 * cos_a1), fill=color, stroke="none", ) path.push("l", (r2 - r1) * sin_a1, (r1 - r2) * cos_a1) path.push( f"a{r2},{r2} 0 0,0 {r2 * (sin_a2 - sin_a1)},{r2 * (cos_a1 - cos_a2)}" ) path.push("l", (r1 - r2) * sin_a2, (r2 - r1) * cos_a2) date_title = str(tracks[0].start_time().date()) str_length = utils.format_float(self.poster.m2u(length)) path.set_desc(title=f"{date_title} {str_length} {self.poster.u()}") g.add(path)
def draw_paths(fn, bbox, paths, pad=(0, 0), sw=0.1): w, h = bbox sx, sy = pad dwg = Drawing(str(fn), size=(w + 2 * sx, h + 2 * sy), profile='tiny', debug=False) bbox_path = tosvgpath(_box(bbox), pad, closed=True) dwg.add(dwg.path(d=bbox_path, stroke=accent, stroke_width=sw, fill='none')) for path in paths: dwg.add( dwg.path(d=tosvgpath(path, pad), stroke=black, fill='none', stroke_width=sw)) dwg.save(pretty=True, indent=2)
def render(self, dwg: Drawing) -> Group: g = dwg.g() p = dwg.path(stroke=self.color, fill_opacity=0, stroke_width=3) p.push("M %f %f" % (-self.width / 2, 0)) p.push("Q %f %f %f %f" % (0, self.width * self.intensity, self.width / 2, 0)) g.add(p) return g
def get_path(self, svg: Drawing, point: np.array): """ Draw icon into SVG file. :param svg: SVG file to draw to :param point: icon position """ shift: np.array = self.offset + point return svg.path(d=self.path, transform=f"translate({shift[0]},{shift[1]})")
def render(self, dwg: Drawing) -> Group: g = dwg.g() p = dwg.path(fill="black", stroke_width=0) p.push("M %f %f" % (-self.width / 2, 0)) p.push("Q %f %f %f %f" % (0, self.width * self.intensity, self.width / 2, 0)) p.push("Z") g.add(p) return g
def _draw_circle_segment(self, d: svgwrite.Drawing, tracks: List[Track], a1: float, a2: float, rr: ValueRange, center: XY): length = sum([t.length for t in tracks]) color = self.color(self.poster.length_range_by_date, length, [t for t in tracks if t.special]) r1 = rr.lower() r2 = rr.lower() + rr.diameter() * length / self.poster.length_range_by_date.upper() sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a2, cos_a2 = math.sin(a2), math.cos(a2) path = d.path(d=('M', center.x + r1 * sin_a1, center.y - r1 * cos_a1), fill=color, stroke='none') path.push('l', (r2 - r1) * sin_a1, (r1 - r2) * cos_a1) path.push('a{},{} 0 0,0 {},{}'.format(r2, r2, r2 * (sin_a2 - sin_a1), r2 * (cos_a1 - cos_a2))) path.push('l', (r1 - r2) * sin_a2, (r2 - r1) * cos_a2) d.add(path)
def draw_path_clip(self): path_filename = "{}/path_clip_{}.svg".format( self.output_folder, basename(self.filename).replace(".svg", "")) dwg = Drawing(path_filename) image_bbox = calc_overall_bbox(self.tile_paths) dx = self.pent_x - min(image_bbox[0], image_bbox[1]) dy = self.pent_y - min(image_bbox[2], image_bbox[3]) dwg.add( dwg.path( **{ "d": self.new_pentagon().d(), "fill": "none", 'stroke-width': 4, 'stroke': rgb(0, 0, 0) })) neg_transform = "translate({}, {})".format(-dx, -dy) transform = "translate({}, {})".format(dx, dy) clip_path = dwg.defs.add( dwg.clipPath(id="pent_path", transform=neg_transform)) clip_path.add(dwg.path(d=self.new_pentagon().d())) group = dwg.add( dwg.g(clip_path="url(#pent_path)", transform=transform, id="clippedpath")) for i, path in enumerate(self.tile_paths): group.add( dwg.path(d=path.d(), style=self.tile_attributes[i].get('style'), id=self.tile_attributes[i]['id'])) dwg.add(dwg.use("#clippedpath", transform="transform(100, 100)")) dwg.viewbox(self.pent_x, self.pent_y, self.pent_width, self.pent_height) dwg.save() xml = xml.dom.minidom.parse(path_filename) open(path_filename, "w").write(xml.toprettyxml())
def add(self, svg: Drawing) -> None: point: np.array = to_grid(self.point) radius: float = 7.5 a1 = self.angle + math.pi / 9.0 a2 = self.angle - math.pi / 9.0 n1 = np.array((math.cos(a1), math.sin(a1))) n2 = np.array((math.cos(a2), math.sin(a2))) p1 = point + n1 * radius p2 = point + n1 * 20 p3 = point + n2 * 20 p4 = point + n2 * radius svg.add( svg.path(d=["M", p1, "C", p2, p3, p4], fill="none", stroke="black", stroke_width=0.5)) n = (p4 - p3) / np.linalg.norm(p4 - p3) svg.add( svg.path(d=create_v(p4, n, np.dot(rotation_matrix(-math.pi / 2.0), n)), stroke_width=0.5, fill="none", stroke="black"))
def draw_single_pentagon(self): pentagon = self.new_pentagon() dwg = Drawing("{}/single_pentagon.svg".format(self.output_folder), profile='tiny') dwg.add( dwg.path( **{ 'd': pentagon.d(), 'fill': "none", 'stroke-width': 4, 'stroke': rgb(0, 0, 0) })) dwg.viewbox(self.pent_x, self.pent_y, self.pent_width, self.pent_height) dwg.save()
def _draw_year(self, d: svgwrite.Drawing, size: XY, offset: XY, year: int): min_size = min(size.x, size.y) outer_radius = 0.5 * min_size - 6 radius_range = ValueRange.from_pair(outer_radius / 4, outer_radius) center = offset + 0.5 * size if self._rings: self._draw_rings(d, center, radius_range) year_style = 'dominant-baseline: central; font-size:{}px; font-family:Arial;'.format(min_size * 4.0 / 80.0) month_style = 'font-size:{}px; font-family:Arial;'.format(min_size * 3.0 / 80.0) d.add(d.text('{}'.format(year), insert=center.tuple(), fill=self.poster.colors['text'], text_anchor="middle", alignment_baseline="middle", style=year_style)) df = 360.0 / (366 if calendar.isleap(year) else 365) day = 0 date = datetime.date(year, 1, 1) while date.year == year: text_date = date.strftime("%Y-%m-%d") a1 = math.radians(day * df) a2 = math.radians((day + 1) * df) if date.day == 1: (_, last_day) = calendar.monthrange(date.year, date.month) a3 = math.radians((day + last_day - 1) * df) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a3, cos_a3 = math.sin(a3), math.cos(a3) r1 = outer_radius + 1 r2 = outer_radius + 6 r3 = outer_radius + 2 d.add(d.line( start=(center + r1 * XY(sin_a1, -cos_a1)).tuple(), end=(center + r2 * XY(sin_a1, -cos_a1)).tuple(), stroke=self.poster.colors['text'], stroke_width=0.3)) path = d.path(d=('M', center.x + r3 * sin_a1, center.y - r3 * cos_a1), fill='none', stroke='none') path.push('a{},{} 0 0,1 {},{}'.format(r3, r3, r3 * (sin_a3 - sin_a1), r3 * (cos_a1 - cos_a3))) d.add(path) tpath = svgwrite.text.TextPath(path, date.strftime("%B"), startOffset=(0.5 * r3 * (a3 - a1))) text = d.text("", fill=self.poster.colors['text'], text_anchor="middle", style=month_style) text.add(tpath) d.add(text) if text_date in self.poster.tracks_by_date: self._draw_circle_segment(d, self.poster.tracks_by_date[text_date], a1, a2, radius_range, center) day += 1 date += datetime.timedelta(1)
class Writer(): def __init__(self, fontfn, outfn, size, pad=0, sw=0.2, nl=10, xdst=1): self.pos = (pad, pad) self.sw = sw self.pad = pad self.nl = nl self.xdst = xdst self.dwg = Drawing(str(outfn), size=size, profile='tiny', debug=False) with open(str(fontfn), 'r') as f: self.symbols = load(f)['symbols'] def newline(self): self.pos = (self.pad, self.pos[1] + self.nl) def scale(self, s): for o in self.symbols.values(): w = o['w'] h = o['h'] new_paths = [] for path in o['paths']: new_path = [] for x, y in path: new_path.append((s*(x-w*0.5)+w*0.5*s, s*(y-h*0.5)+h*0.5*s)) new_paths.append(new_path) o.update({'w': w*s, 'h': h*s, 'paths': new_paths}) return self def write(self, phrase): for s in phrase: if s in self.symbols: o = self.symbols[s] gw = o['w'] paths = o['paths'] for path in paths: self.dwg.add( self.dwg.path( d=tosvgpath(list(shift_path(path, self.pos))), stroke=black, fill='none', stroke_width=self.sw)) self.pos = _rel_move(self.pos, (gw + self.xdst, 0)) else: print('symbol not found: {:s}'.format(s)) self.dwg.save(pretty=True, indent=2)
def export_svg_svgwrite(fn, paths, w, h, line_width=0.1): from svgwrite import Drawing w_str = "{}pt".format(w) h_str = "{}pt".format(h) dwg = Drawing(filename = fn, size = (w_str, h_str), viewBox=("0 0 {} {}".format(w,h))) for path in paths: if(len(path) > 1): str_list = [] str_list.append("M {},{}".format(path[0,0],path[0,1])) for e in path[1:]: str_list.append(" L {},{}".format(e[0],e[1])) s = ''.join(str_list) dwg.add(dwg.path(s).stroke(color="rgb(0%,0%,0%)",width=line_width).fill("none")) dwg.save()
def add(self, svg: Drawing) -> None: a: np.array = to_grid(self.point1) b: np.array = to_grid(self.point2) n = (b - a) / np.linalg.norm((b - a)) na = a + (n * self.radius) nb = b - (n * self.radius) line = svg.line(na, nb, stroke_width=0.5, fill="none", stroke="black") if not self.is_feasible: line.update({"stroke-dasharray": "1,1"}) svg.add(line) v = svg.path(d=create_v(nb, n, np.dot(rotation_matrix(-math.pi / 2.0), n)), stroke_width=0.5, fill="none", stroke="black") if not self.is_feasible: v.update({"stroke-dasharray": "1,1"}) svg.add(v)
def generate_tiling(self): dwg = Drawing("{}/tiling2.svg".format(self.output_folder), profile="tiny") current_color = 0 row_spacing = self.pent_height * 2 + self.bottom_length for y in range(self.num_down): transform = "translate({}, {})".format(0, self.rep_spacing * y) dgroup = dwg.add(dwg.g(transform=transform)) for x in range(self.num_across): # if x is odd, point 1 of pent 1 needs to be attached to point 3 of pent 2 if x % 2 == 1: dx = int( x / 2 ) * self.rep_spacing + self.pent_width * 2 + self.column_offset.real transform = "translate({}, {})".format( dx, self.column_offset.imag) else: transform = "translate({}, {})".format( int(x / 2) * self.rep_spacing, 0) group = dgroup.add(dwg.g(transform=transform)) for pent in self.cairo_group: group.add( dwg.path( **{ 'd': pent.d(), 'fill': self._colors[current_color % len(self._colors)], 'stroke-width': 4, 'stroke': rgb(0, 0, 0) })) current_color += 1 dwg.viewbox(*self.pattern_viewbox) dwg.save(pretty=True)
def export_svg_svgwrite(fn, paths, w, h, line_width=0.1): from svgwrite import Drawing w_str = "{}pt".format(w) h_str = "{}pt".format(h) dwg = Drawing(filename=fn, size=(w_str, h_str), viewBox=("0 0 {} {}".format(w, h))) for path in paths: if (len(path) > 1): str_list = [] str_list.append("M {},{}".format(path[0, 0], path[0, 1])) for e in path[1:]: str_list.append(" L {},{}".format(e[0], e[1])) s = ''.join(str_list) dwg.add( dwg.path(s).stroke(color="rgb(0%,0%,0%)", width=line_width).fill("none")) dwg.save()
def save_pie_chart(filename, root_list, step_size): # create the drawing surface svg_drawing = Drawing(filename=filename, size=(SVG_SIZE, SVG_SIZE), debug=True) start_x = SVG_SIZE // 2 start_y = SVG_SIZE // 2 radius = SVG_SIZE // 2 all_angles = [] for node in root_list: all_angles += node.pie_angle all_angles = sorted(all_angles) radians0 = all_angles[-1] for i in range(len(all_angles)): radians1 = all_angles[i] dx0 = radius * (math.sin(radians0)) dy0 = radius * (math.cos(radians0)) dx1 = radius * (math.sin(radians1)) dy1 = radius * (math.cos(radians1)) m0 = dy0 n0 = -dx0 m1 = -dy0 + dy1 n1 = dx0 - dx1 w = svg_drawing.path( d="M {0},{1} l {2},{3} a {4},{4} 0 0,0 {5},{6} z".format( start_x, start_y, m0, n0, radius, m1, n1), fill=colors[i], stroke="none", ) svg_drawing.add(w) radians0 = radians1 svg_drawing.save()
def disvg(paths=None, colors=None, filename=None, stroke_widths=None, nodes=None, node_colors=None, node_radii=None, openinbrowser=True, timestamp=None, margin_size=0.1, mindim=600, dimensions=None, viewbox=None, text=None, text_path=None, font_size=None, attributes=None, svg_attributes=None, svgwrite_debug=False, paths2Drawing=False, baseunit='px'): """Creates (and optionally displays) an SVG file. REQUIRED INPUTS: :param paths - a list of paths OPTIONAL INPUT: :param colors - specifies the path stroke color. By default all paths will be black (#000000). This paramater can be input in a few ways 1) a list of strings that will be input into the path elements stroke attribute (so anything that is understood by the svg viewer). 2) a string of single character colors -- e.g. setting colors='rrr' is equivalent to setting colors=['red', 'red', 'red'] (see the 'color_dict' dictionary above for a list of possibilities). 3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...]. :param filename - the desired location/filename of the SVG file created (by default the SVG will be named 'disvg_output.svg' or 'disvg_output_<timestamp>.svg' and stored in the temporary directory returned by `tempfile.gettempdir()`. See `timestamp` for information on the timestamp. :param stroke_widths - a list of stroke_widths to use for paths (default is 0.5% of the SVG's width or length) :param nodes - a list of points to draw as filled-in circles :param node_colors - a list of colors to use for the nodes (by default nodes will be red) :param node_radii - a list of radii to use for the nodes (by default nodes will be radius will be 1 percent of the svg's width/length) :param text - string or list of strings to be displayed :param text_path - if text is a list, then this should be a list of path (or path segments of the same length. Note: the path must be long enough to display the text or the text will be cropped by the svg viewer. :param font_size - a single float of list of floats. :param openinbrowser - Set to True to automatically open the created SVG in the user's default web browser. :param timestamp - if true, then the a timestamp will be appended to the output SVG's filename. This is meant as a workaround for issues related to rapidly opening multiple SVGs in your browser using `disvg`. This defaults to true if `filename is None` and false otherwise. :param margin_size - The min margin (empty area framing the collection of paths) size used for creating the canvas and background of the SVG. :param mindim - The minimum dimension (height or width) of the output SVG (default is 600). :param dimensions - The (x,y) display dimensions of the output SVG. I.e. this specifies the `width` and `height` SVG attributes. Note that these also can be used to specify units other than pixels. Using this will override the `mindim` parameter. :param viewbox - This specifies the coordinated system used in the svg. The SVG `viewBox` attribute works together with the the `height` and `width` attrinutes. Using these three attributes allows for shifting and scaling of the SVG canvas without changing the any values other than those in `viewBox`, `height`, and `width`. `viewbox` should be input as a 4-tuple, (min_x, min_y, width, height), or a string "min_x min_y width height". Using this will override the `mindim` parameter. :param attributes - a list of dictionaries of attributes for the input paths. Note: This will override any other conflicting settings. :param svg_attributes - a dictionary of attributes for output svg. :param svgwrite_debug - This parameter turns on/off `svgwrite`'s debugging mode. By default svgwrite_debug=False. This increases speed and also prevents `svgwrite` from raising of an error when not all `svg_attributes` key-value pairs are understood. :param paths2Drawing - If true, an `svgwrite.Drawing` object is returned and no file is written. This `Drawing` can later be saved using the `svgwrite.Drawing.save()` method. NOTES: * The `svg_attributes` parameter will override any other conflicting settings. * Any `extra` parameters that `svgwrite.Drawing()` accepts can be controlled by passing them in through `svg_attributes`. * The unit of length here is assumed to be pixels in all variables. * If this function is used multiple times in quick succession to display multiple SVGs (all using the default filename), the svgviewer/browser will likely fail to load some of the SVGs in time. To fix this, use the timestamp attribute, or give the files unique names, or use a pause command (e.g. time.sleep(1)) between uses. SEE ALSO: * document.py """ _default_relative_node_radius = 5e-3 _default_relative_stroke_width = 1e-3 _default_path_color = '#000000' # black _default_node_color = '#ff0000' # red _default_font_size = 12 if filename is None: timestamp = True if timestamp is None else timestamp filename = os_path.join(gettempdir(), 'disvg_output.svg') dirname = os_path.abspath(os_path.dirname(filename)) if not os_path.exists(dirname): makedirs(dirname) # append time stamp to filename if timestamp: fbname, fext = os_path.splitext(filename) tstamp = str(time()).replace('.', '') stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext filename = os_path.join(dirname, stfilename) # check paths and colors are set if isinstance(paths, Path) or is_path_segment(paths): paths = [paths] if paths: if not colors: colors = [_default_path_color] * len(paths) else: assert len(colors) == len(paths) if isinstance(colors, str): colors = str2colorlist(colors, default_color=_default_path_color) elif isinstance(colors, list): for idx, c in enumerate(colors): if is3tuple(c): colors[idx] = "rgb" + str(c) # check nodes and nodes_colors are set (node_radii are set later) if nodes: if not node_colors: node_colors = [_default_node_color] * len(nodes) else: assert len(node_colors) == len(nodes) if isinstance(node_colors, str): node_colors = str2colorlist(node_colors, default_color=_default_node_color) elif isinstance(node_colors, list): for idx, c in enumerate(node_colors): if is3tuple(c): node_colors[idx] = "rgb" + str(c) # set up the viewBox and display dimensions of the output SVG # along the way, set stroke_widths and node_radii if not provided assert paths or nodes stuff2bound = [] if viewbox: if not isinstance(viewbox, str): viewbox = '%s %s %s %s' % viewbox if dimensions is None: dimensions = viewbox.split(' ')[2:4] elif dimensions: dimensions = tuple(map(str, dimensions)) def strip_units(s): return re.search(r'\d*\.?\d*', s.strip()).group() viewbox = '0 0 %s %s' % tuple(map(strip_units, dimensions)) else: if paths: stuff2bound += paths if nodes: stuff2bound += nodes if text_path: stuff2bound += text_path xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound) dx = xmax - xmin dy = ymax - ymin if dx == 0: dx = 1 if dy == 0: dy = 1 # determine stroke_widths to use (if not provided) and max_stroke_width if paths: if not stroke_widths: sw = max(dx, dy) * _default_relative_stroke_width stroke_widths = [sw] * len(paths) max_stroke_width = sw else: assert len(paths) == len(stroke_widths) max_stroke_width = max(stroke_widths) else: max_stroke_width = 0 # determine node_radii to use (if not provided) and max_node_diameter if nodes: if not node_radii: r = max(dx, dy) * _default_relative_node_radius node_radii = [r] * len(nodes) max_node_diameter = 2 * r else: assert len(nodes) == len(node_radii) max_node_diameter = 2 * max(node_radii) else: max_node_diameter = 0 extra_space_for_style = max(max_stroke_width, max_node_diameter) xmin -= margin_size * dx + extra_space_for_style / 2 ymin -= margin_size * dy + extra_space_for_style / 2 dx += 2 * margin_size * dx + extra_space_for_style dy += 2 * margin_size * dy + extra_space_for_style viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy) if mindim is None: szx = "{}{}".format(dx, baseunit) szy = "{}{}".format(dy, baseunit) else: if dx > dy: szx = str(mindim) + baseunit szy = str(int(ceil(mindim * dy / dx))) + baseunit else: szx = str(int(ceil(mindim * dx / dy))) + baseunit szy = str(mindim) + baseunit dimensions = szx, szy # Create an SVG file if svg_attributes is not None: dimensions = (svg_attributes.get("width", dimensions[0]), svg_attributes.get("height", dimensions[1])) debug = svg_attributes.get("debug", svgwrite_debug) dwg = Drawing(filename=filename, size=dimensions, debug=debug, **svg_attributes) else: dwg = Drawing(filename=filename, size=dimensions, debug=svgwrite_debug, viewBox=viewbox) # add paths if paths: for i, p in enumerate(paths): if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p if attributes: good_attribs = {'d': ps} for key in attributes[i]: val = attributes[i][key] if key != 'd': try: dwg.path(ps, **{key: val}) good_attribs.update({key: val}) except Exception as e: warn(str(e)) dwg.add(dwg.path(**good_attribs)) else: dwg.add( dwg.path(ps, stroke=colors[i], stroke_width=str(stroke_widths[i]), fill='none')) # add nodes (filled in circles) if nodes: for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]): dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt])) # add texts if text: assert isinstance(text, str) or (isinstance(text, list) and isinstance( text_path, list) and len(text_path) == len(text)) if isinstance(text, str): text = [text] if not font_size: font_size = [_default_font_size] if not text_path: pos = complex(xmin + margin_size * dx, ymin + margin_size * dy) text_path = [Line(pos, pos + 1).d()] else: if font_size: if isinstance(font_size, list): assert len(font_size) == len(text) else: font_size = [font_size] * len(text) else: font_size = [_default_font_size] * len(text) for idx, s in enumerate(text): p = text_path[idx] if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p # paragraph = dwg.add(dwg.g(font_size=font_size[idx])) # paragraph.add(dwg.textPath(ps, s)) pathid = 'tp' + str(idx) dwg.defs.add(dwg.path(d=ps, id=pathid)) txter = dwg.add(dwg.text('', font_size=font_size[idx])) txter.add(txt.TextPath('#' + pathid, s)) if paths2Drawing: return dwg dwg.save() # re-open the svg, make the xml pretty, and save it again xmlstring = md_xml_parse(filename).toprettyxml() with open(filename, 'w') as f: f.write(xmlstring) # try to open in web browser if openinbrowser: try: open_in_browser(filename) except: print("Failed to open output SVG in browser. SVG saved to:") print(filename)
def _draw_year(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY, year: int) -> None: min_size = min(size.x, size.y) outer_radius = 0.5 * min_size - 6 radius_range = ValueRange.from_pair(outer_radius / 4, outer_radius) center = offset + 0.5 * size if self._rings: self._draw_rings(dr, g, center, radius_range) year_style = f"dominant-baseline: central; font-size:{min_size * 4.0 / 80.0}px; font-family:Arial;" month_style = f"font-size:{min_size * 3.0 / 80.0}px; font-family:Arial;" g.add( dr.text( f"{year}", insert=center.tuple(), fill=self.poster.colors["text"], text_anchor="middle", alignment_baseline="middle", style=year_style, )) df = 360.0 / (366 if calendar.isleap(year) else 365) day = 0 date = datetime.date(year, 1, 1) while date.year == year: text_date = date.strftime("%Y-%m-%d") a1 = math.radians(day * df) a2 = math.radians((day + 1) * df) if date.day == 1: (_, last_day) = calendar.monthrange(date.year, date.month) a3 = math.radians((day + last_day - 1) * df) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a3, cos_a3 = math.sin(a3), math.cos(a3) r1 = outer_radius + 1 r2 = outer_radius + 6 r3 = outer_radius + 2 g.add( dr.line( start=(center + r1 * XY(sin_a1, -cos_a1)).tuple(), end=(center + r2 * XY(sin_a1, -cos_a1)).tuple(), stroke=self.poster.colors["text"], stroke_width=0.3, )) path = dr.path( d=("M", center.x + r3 * sin_a1, center.y - r3 * cos_a1), fill="none", stroke="none", ) path.push( f"a{r3},{r3} 0 0,1 {r3 * (sin_a3 - sin_a1)},{r3 * (cos_a1 - cos_a3)}" ) g.add(path) tpath = svgwrite.text.TextPath( path, self.poster.month_name(date.month), startOffset=(0.5 * r3 * (a3 - a1))) text = dr.text( "", fill=self.poster.colors["text"], text_anchor="middle", style=month_style, ) text.add(tpath) g.add(text) if text_date in self.poster.tracks_by_date: self._draw_circle_segment( dr, g, self.poster.tracks_by_date[text_date], a1, a2, radius_range, center, ) day += 1 date += datetime.timedelta(1)
def flatten_scene(pScene): lNode = pScene.GetRootNode() if not lNode: return for i in range(lNode.GetChildCount()): lChildNode = lNode.GetChild(i) if lChildNode.GetNodeAttribute() is None: continue lAttributeType = (lChildNode.GetNodeAttribute().GetAttributeType()) if lAttributeType != FbxNodeAttribute.eMesh: continue lMesh = lChildNode.GetNodeAttribute() projected_points = {} control_points = lMesh.GetControlPoints() start_point = 0 poly_paths = [] for polygon_num in range(lMesh.GetPolygonCount()): corners = [] for corner in range(3): corners.append(lMesh.GetPolygonVertex(polygon_num, corner)) # first, check if any of the control points are already projected flattened = [] for j, corner in enumerate(corners): if corner in projected_points: flattened.append(projected_points[corner]) continue target_corner = corners[j - 1] current_vec = control_points[corner] target_vec = control_points[target_corner] angle = acos( current_vec.DotProduct(target_vec) / (current_vec.Length() * target_vec.Length())) length = current_vec.Distance(target_vec) # find where the last point was. If it doesn't exist, use the start point start_corner = projected_points[target_corner] \ if target_corner in projected_points else start_point flattened_corner = start_corner + length * (cos(angle) + 1j * sin(angle)) projected_points[corner] = flattened_corner start_point = flattened_corner flattened.append(flattened_corner) poly_paths.append( Path(*[ Line(start=flattened[j], end=flattened[j - 1]) for j in range(3) ])) dwg = Drawing("mesh{}.svg".format(i), profile='tiny') for poly_path in poly_paths: dwg.add( dwg.path( **{ 'd': poly_path.d(), 'fill': "none", 'stroke-width': 4, 'stroke': rgb(0, 0, 0) })) bbox = calc_overall_bbox(poly_paths) width, height = abs(bbox[1] - bbox[0]), abs(bbox[3] - bbox[2]) dwg.viewbox(min(bbox[0], bbox[1]), min(bbox[2], bbox[3]), width, height) dwg.save()
def add_graphelement_to_svg_drawing(element: GraphElement, drawing: svgwrite.Drawing, filters: Dict[str, Filter]) -> None: args = {} for attr, value in element.attr.items(): if attr.startswith('.svg_tag'): continue if attr.startswith('.svg_'): name = attr[5:] if name == 'filter': args[name] = filters[value].get_funciri() else: args[name] = value if '.svg_tag' in element.attr: tag = element.attr['.svg_tag'] if tag == 'rect': x = float(element.attr['x']) y = -float(element.attr['y']) width = float(element.attr.get('.svg_width', 0.1)) height = float(element.attr.get('.svg_height', 0.1)) x = x - width / 2 y = y - height / 2 drawing.add(drawing.rect((x*mult, y*mult), (width*mult, height*mult), **args)) elif tag == 'path': drawing.add(drawing.path(**args)) elif tag == 'circle': x = float(element.attr['x']) y = -float(element.attr['y']) args.setdefault('r', '1cm') args.setdefault('stroke_width', '0.1mm') args.setdefault('stroke', 'black') args.setdefault('fill', 'none') drawing.add(drawing.circle(center=(x * mult, y * mult), **args)) elif tag == 'image': x = float(element.attr['x']) y = -float(element.attr['y']) width = float(element.attr.pop('.svg_width', 5)) height = float(element.attr.pop('.svg_height', 5)) x = x - width / 2 y = y - height / 2 center = ((x + width / 2), (y + height / 2)) args.setdefault('insert', (x * mult, y * mult)) args.setdefault('size', (width * mult, height * mult)) if '.svgx_rotate' in element.attr: rotation = float(element.attr['.svgx_rotate']) args.setdefault('transform', f'translate({center[0]*mult}, {center[1]*mult}) ' f'rotate({-rotation}) ' f'translate({-center[0]*mult}, {-center[1]*mult})' ) drawing.add(getattr(drawing, element.attr['.svg_tag'])(**args)) elif tag != 'None' and tag is not None: drawing.add(getattr(drawing, element.attr['.svg_tag'])(**args)) elif isinstance(element, Vertex): if '.helper_node' in element.attr and element.attr['.helper_node']: return x = float(element.attr['x']) y = -float(element.attr['y']) args.setdefault('r', '0.4cm') args.setdefault('stroke_width', '1mm') args.setdefault('stroke', 'black') args.setdefault('fill', 'none') drawing.add(drawing.circle(center=(x*mult, y*mult), **args)) elif isinstance(element, Edge): v1 = element.vertex1 v2 = element.vertex2 x1 = float(v1.attr['x']) y1 = -float(v1.attr['y']) x2 = float(v2.attr['x']) y2 = -float(v2.attr['y']) args.setdefault('stroke_width', '1mm') args.setdefault('stroke', 'black') drawing.add(drawing.line(start=(x1*mult, y1*mult), end=(x2*mult, y2*mult), **args)) else: raise ValueError
def disvg(paths=None, colors=None, filename=os_path.join(getcwd(), 'disvg_output.svg'), stroke_widths=None, nodes=None, node_colors=None, node_radii=None, openinbrowser=True, timestamp=False, margin_size=0.1, mindim=600, dimensions=None, viewbox=None, text=None, text_path=None, font_size=None, attributes=None, svg_attributes=None): """Takes in a list of paths and creates an SVG file containing said paths. REQUIRED INPUTS: :param paths - a list of paths OPTIONAL INPUT: :param colors - specifies the path stroke color. By default all paths will be black (#000000). This paramater can be input in a few ways 1) a list of strings that will be input into the path elements stroke attribute (so anything that is understood by the svg viewer). 2) a string of single character colors -- e.g. setting colors='rrr' is equivalent to setting colors=['red', 'red', 'red'] (see the 'color_dict' dictionary above for a list of possibilities). 3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...]. :param filename - the desired location/filename of the SVG file created (by default the SVG will be stored in the current working directory and named 'disvg_output.svg'). :param stroke_widths - a list of stroke_widths to use for paths (default is 0.5% of the SVG's width or length) :param nodes - a list of points to draw as filled-in circles :param node_colors - a list of colors to use for the nodes (by default nodes will be red) :param node_radii - a list of radii to use for the nodes (by default nodes will be radius will be 1 percent of the svg's width/length) :param text - string or list of strings to be displayed :param text_path - if text is a list, then this should be a list of path (or path segments of the same length. Note: the path must be long enough to display the text or the text will be cropped by the svg viewer. :param font_size - a single float of list of floats. :param openinbrowser - Set to True to automatically open the created SVG in the user's default web browser. :param timestamp - if True, then the a timestamp will be appended to the output SVG's filename. This will fix issues with rapidly opening multiple SVGs in your browser. :param margin_size - The min margin (empty area framing the collection of paths) size used for creating the canvas and background of the SVG. :param mindim - The minimum dimension (height or width) of the output SVG (default is 600). :param dimensions - The display dimensions of the output SVG. Using this will override the mindim parameter. :param viewbox - This specifies what rectangular patch of R^2 will be viewable through the outputSVG. It should be input in the form (min_x, min_y, width, height). This is different from the display dimension of the svg, which can be set through mindim or dimensions. :param attributes - a list of dictionaries of attributes for the input paths. Note: This will override any other conflicting settings. :param svg_attributes - a dictionary of attributes for output svg. Note 1: This will override any other conflicting settings. Note 2: Setting `svg_attributes={'debug': False}` may result in a significant increase in speed. NOTES: -The unit of length here is assumed to be pixels in all variables. -If this function is used multiple times in quick succession to display multiple SVGs (all using the default filename), the svgviewer/browser will likely fail to load some of the SVGs in time. To fix this, use the timestamp attribute, or give the files unique names, or use a pause command (e.g. time.sleep(1)) between uses. """ _default_relative_node_radius = 5e-3 _default_relative_stroke_width = 1e-3 _default_path_color = '#000000' # black _default_node_color = '#ff0000' # red _default_font_size = 12 # append directory to filename (if not included) if os_path.dirname(filename) == '': filename = os_path.join(getcwd(), filename) # append time stamp to filename if timestamp: fbname, fext = os_path.splitext(filename) dirname = os_path.dirname(filename) tstamp = str(time()).replace('.', '') stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext filename = os_path.join(dirname, stfilename) # check paths and colors are set if isinstance(paths, Path) or is_path_segment(paths): paths = [paths] if paths: if not colors: colors = [_default_path_color] * len(paths) else: assert len(colors) == len(paths) if isinstance(colors, str): colors = str2colorlist(colors, default_color=_default_path_color) elif isinstance(colors, list): for idx, c in enumerate(colors): if is3tuple(c): colors[idx] = "rgb" + str(c) # check nodes and nodes_colors are set (node_radii are set later) if nodes: if not node_colors: node_colors = [_default_node_color] * len(nodes) else: assert len(node_colors) == len(nodes) if isinstance(node_colors, str): node_colors = str2colorlist(node_colors, default_color=_default_node_color) elif isinstance(node_colors, list): for idx, c in enumerate(node_colors): if is3tuple(c): node_colors[idx] = "rgb" + str(c) # set up the viewBox and display dimensions of the output SVG # along the way, set stroke_widths and node_radii if not provided assert paths or nodes stuff2bound = [] if viewbox: szx, szy = viewbox[2:4] else: if paths: stuff2bound += paths if nodes: stuff2bound += nodes if text_path: stuff2bound += text_path xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound) dx = xmax - xmin dy = ymax - ymin if dx == 0: dx = 1 if dy == 0: dy = 1 # determine stroke_widths to use (if not provided) and max_stroke_width if paths: if not stroke_widths: sw = max(dx, dy) * _default_relative_stroke_width stroke_widths = [sw] * len(paths) max_stroke_width = sw else: assert len(paths) == len(stroke_widths) max_stroke_width = max(stroke_widths) else: max_stroke_width = 0 # determine node_radii to use (if not provided) and max_node_diameter if nodes: if not node_radii: r = max(dx, dy) * _default_relative_node_radius node_radii = [r] * len(nodes) max_node_diameter = 2 * r else: assert len(nodes) == len(node_radii) max_node_diameter = 2 * max(node_radii) else: max_node_diameter = 0 extra_space_for_style = max(max_stroke_width, max_node_diameter) xmin -= margin_size * dx + extra_space_for_style / 2 ymin -= margin_size * dy + extra_space_for_style / 2 dx += 2 * margin_size * dx + extra_space_for_style dy += 2 * margin_size * dy + extra_space_for_style viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy) if dimensions: szx, szy = dimensions else: if dx > dy: szx = str(mindim) + 'px' szy = str(int(ceil(mindim * dy / dx))) + 'px' else: szx = str(int(ceil(mindim * dx / dy))) + 'px' szy = str(mindim) + 'px' # Create an SVG file if svg_attributes: szx = svg_attributes.get("width", szx) szy = svg_attributes.get("height", szy) dwg = Drawing(filename=filename, size=(szx, szy), **svg_attributes) else: dwg = Drawing(filename=filename, size=(szx, szy), viewBox=viewbox) # add paths if paths: for i, p in enumerate(paths): if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p if attributes: good_attribs = {'d': ps} for key in attributes[i]: val = attributes[i][key] if key != 'd': try: dwg.path(ps, **{key: val}) good_attribs.update({key: val}) except Exception as e: warn(str(e)) dwg.add(dwg.path(**good_attribs)) else: dwg.add( dwg.path(ps, stroke=colors[i], stroke_width=str(stroke_widths[i]), fill='none')) # add nodes (filled in circles) if nodes: for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]): dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt])) # add texts if text: assert isinstance(text, str) or (isinstance(text, list) and isinstance( text_path, list) and len(text_path) == len(text)) if isinstance(text, str): text = [text] if not font_size: font_size = [_default_font_size] if not text_path: pos = complex(xmin + margin_size * dx, ymin + margin_size * dy) text_path = [Line(pos, pos + 1).d()] else: if font_size: if isinstance(font_size, list): assert len(font_size) == len(text) else: font_size = [font_size] * len(text) else: font_size = [_default_font_size] * len(text) for idx, s in enumerate(text): p = text_path[idx] if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p # paragraph = dwg.add(dwg.g(font_size=font_size[idx])) # paragraph.add(dwg.textPath(ps, s)) pathid = 'tp' + str(idx) dwg.defs.add(dwg.path(d=ps, id=pathid)) txter = dwg.add(dwg.text('', font_size=font_size[idx])) txter.add(txt.TextPath('#' + pathid, s)) # save svg if not os_path.exists(os_path.dirname(filename)): makedirs(os_path.dirname(filename)) dwg.save() # re-open the svg, make the xml pretty, and save it again xmlstring = md_xml_parse(filename).toprettyxml() with open(filename, 'w') as f: f.write(xmlstring) # try to open in web browser if openinbrowser: try: open_in_browser(filename) except: print("Failed to open output SVG in browser. SVG saved to:") print(filename)
def disvg(paths=None, colors=None, filename=os_path.join(getcwd(), 'disvg_output.svg'), stroke_widths=None, nodes=None, node_colors=None, node_radii=None, openinbrowser=True, timestamp=False, margin_size=0.1, mindim=600, dimensions=None, viewbox=None, text=None, text_path=None, font_size=None, attributes=None, svg_attributes=None, svgwrite_debug=False, paths2Drawing=False): """Takes in a list of paths and creates an SVG file containing said paths. REQUIRED INPUTS: :param paths - a list of paths OPTIONAL INPUT: :param colors - specifies the path stroke color. By default all paths will be black (#000000). This paramater can be input in a few ways 1) a list of strings that will be input into the path elements stroke attribute (so anything that is understood by the svg viewer). 2) a string of single character colors -- e.g. setting colors='rrr' is equivalent to setting colors=['red', 'red', 'red'] (see the 'color_dict' dictionary above for a list of possibilities). 3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...]. :param filename - the desired location/filename of the SVG file created (by default the SVG will be stored in the current working directory and named 'disvg_output.svg'). :param stroke_widths - a list of stroke_widths to use for paths (default is 0.5% of the SVG's width or length) :param nodes - a list of points to draw as filled-in circles :param node_colors - a list of colors to use for the nodes (by default nodes will be red) :param node_radii - a list of radii to use for the nodes (by default nodes will be radius will be 1 percent of the svg's width/length) :param text - string or list of strings to be displayed :param text_path - if text is a list, then this should be a list of path (or path segments of the same length. Note: the path must be long enough to display the text or the text will be cropped by the svg viewer. :param font_size - a single float of list of floats. :param openinbrowser - Set to True to automatically open the created SVG in the user's default web browser. :param timestamp - if True, then the a timestamp will be appended to the output SVG's filename. This will fix issues with rapidly opening multiple SVGs in your browser. :param margin_size - The min margin (empty area framing the collection of paths) size used for creating the canvas and background of the SVG. :param mindim - The minimum dimension (height or width) of the output SVG (default is 600). :param dimensions - The (x,y) display dimensions of the output SVG. I.e. this specifies the `width` and `height` SVG attributes. Note that these also can be used to specify units other than pixels. Using this will override the `mindim` parameter. :param viewbox - This specifies the coordinated system used in the svg. The SVG `viewBox` attribute works together with the the `height` and `width` attrinutes. Using these three attributes allows for shifting and scaling of the SVG canvas without changing the any values other than those in `viewBox`, `height`, and `width`. `viewbox` should be input as a 4-tuple, (min_x, min_y, width, height), or a string "min_x min_y width height". Using this will override the `mindim` parameter. :param attributes - a list of dictionaries of attributes for the input paths. Note: This will override any other conflicting settings. :param svg_attributes - a dictionary of attributes for output svg. :param svgwrite_debug - This parameter turns on/off `svgwrite`'s debugging mode. By default svgwrite_debug=False. This increases speed and also prevents `svgwrite` from raising of an error when not all `svg_attributes` key-value pairs are understood. :param paths2Drawing - If true, an `svgwrite.Drawing` object is returned and no file is written. This `Drawing` can later be saved using the `svgwrite.Drawing.save()` method. NOTES: * The `svg_attributes` parameter will override any other conflicting settings. * Any `extra` parameters that `svgwrite.Drawing()` accepts can be controlled by passing them in through `svg_attributes`. * The unit of length here is assumed to be pixels in all variables. * If this function is used multiple times in quick succession to display multiple SVGs (all using the default filename), the svgviewer/browser will likely fail to load some of the SVGs in time. To fix this, use the timestamp attribute, or give the files unique names, or use a pause command (e.g. time.sleep(1)) between uses. """ _default_relative_node_radius = 5e-3 _default_relative_stroke_width = 1e-3 _default_path_color = '#000000' # black _default_node_color = '#ff0000' # red _default_font_size = 12 # append directory to filename (if not included) if os_path.dirname(filename) == '': filename = os_path.join(getcwd(), filename) # append time stamp to filename if timestamp: fbname, fext = os_path.splitext(filename) dirname = os_path.dirname(filename) tstamp = str(time()).replace('.', '') stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext filename = os_path.join(dirname, stfilename) # check paths and colors are set if isinstance(paths, Path) or is_path_segment(paths): paths = [paths] if paths: if not colors: colors = [_default_path_color] * len(paths) else: assert len(colors) == len(paths) if isinstance(colors, str): colors = str2colorlist(colors, default_color=_default_path_color) elif isinstance(colors, list): for idx, c in enumerate(colors): if is3tuple(c): colors[idx] = "rgb" + str(c) # check nodes and nodes_colors are set (node_radii are set later) if nodes: if not node_colors: node_colors = [_default_node_color] * len(nodes) else: assert len(node_colors) == len(nodes) if isinstance(node_colors, str): node_colors = str2colorlist(node_colors, default_color=_default_node_color) elif isinstance(node_colors, list): for idx, c in enumerate(node_colors): if is3tuple(c): node_colors[idx] = "rgb" + str(c) # set up the viewBox and display dimensions of the output SVG # along the way, set stroke_widths and node_radii if not provided assert paths or nodes stuff2bound = [] if viewbox: if not isinstance(viewbox, str): viewbox = '%s %s %s %s' % viewbox if dimensions is None: dimensions = viewbox.split(' ')[2:4] elif dimensions: dimensions = tuple(map(str, dimensions)) def strip_units(s): return re.search(r'\d*\.?\d*', s.strip()).group() viewbox = '0 0 %s %s' % tuple(map(strip_units, dimensions)) else: if paths: stuff2bound += paths if nodes: stuff2bound += nodes if text_path: stuff2bound += text_path xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound) dx = xmax - xmin dy = ymax - ymin if dx == 0: dx = 1 if dy == 0: dy = 1 # determine stroke_widths to use (if not provided) and max_stroke_width if paths: if not stroke_widths: sw = max(dx, dy) * _default_relative_stroke_width stroke_widths = [sw]*len(paths) max_stroke_width = sw else: assert len(paths) == len(stroke_widths) max_stroke_width = max(stroke_widths) else: max_stroke_width = 0 # determine node_radii to use (if not provided) and max_node_diameter if nodes: if not node_radii: r = max(dx, dy) * _default_relative_node_radius node_radii = [r]*len(nodes) max_node_diameter = 2*r else: assert len(nodes) == len(node_radii) max_node_diameter = 2*max(node_radii) else: max_node_diameter = 0 extra_space_for_style = max(max_stroke_width, max_node_diameter) xmin -= margin_size*dx + extra_space_for_style/2 ymin -= margin_size*dy + extra_space_for_style/2 dx += 2*margin_size*dx + extra_space_for_style dy += 2*margin_size*dy + extra_space_for_style viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy) if dx > dy: szx = str(mindim) + 'px' szy = str(int(ceil(mindim * dy / dx))) + 'px' else: szx = str(int(ceil(mindim * dx / dy))) + 'px' szy = str(mindim) + 'px' dimensions = szx, szy # Create an SVG file if svg_attributes is not None: dimensions = (svg_attributes.get("width", dimensions[0]), svg_attributes.get("height", dimensions[1])) debug = svg_attributes.get("debug", svgwrite_debug) dwg = Drawing(filename=filename, size=dimensions, debug=debug, **svg_attributes) else: dwg = Drawing(filename=filename, size=dimensions, debug=svgwrite_debug, viewBox=viewbox) # add paths if paths: for i, p in enumerate(paths): if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p if attributes: good_attribs = {'d': ps} for key in attributes[i]: val = attributes[i][key] if key != 'd': try: dwg.path(ps, **{key: val}) good_attribs.update({key: val}) except Exception as e: warn(str(e)) dwg.add(dwg.path(**good_attribs)) else: dwg.add(dwg.path(ps, stroke=colors[i], stroke_width=str(stroke_widths[i]), fill='none')) # add nodes (filled in circles) if nodes: for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]): dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt])) # add texts if text: assert isinstance(text, str) or (isinstance(text, list) and isinstance(text_path, list) and len(text_path) == len(text)) if isinstance(text, str): text = [text] if not font_size: font_size = [_default_font_size] if not text_path: pos = complex(xmin + margin_size*dx, ymin + margin_size*dy) text_path = [Line(pos, pos + 1).d()] else: if font_size: if isinstance(font_size, list): assert len(font_size) == len(text) else: font_size = [font_size] * len(text) else: font_size = [_default_font_size] * len(text) for idx, s in enumerate(text): p = text_path[idx] if isinstance(p, Path): ps = p.d() elif is_path_segment(p): ps = Path(p).d() else: # assume this path, p, was input as a Path d-string ps = p # paragraph = dwg.add(dwg.g(font_size=font_size[idx])) # paragraph.add(dwg.textPath(ps, s)) pathid = 'tp' + str(idx) dwg.defs.add(dwg.path(d=ps, id=pathid)) txter = dwg.add(dwg.text('', font_size=font_size[idx])) txter.add(txt.TextPath('#'+pathid, s)) if paths2Drawing: return dwg # save svg if not os_path.exists(os_path.dirname(filename)): makedirs(os_path.dirname(filename)) dwg.save() # re-open the svg, make the xml pretty, and save it again xmlstring = md_xml_parse(filename).toprettyxml() with open(filename, 'w') as f: f.write(xmlstring) # try to open in web browser if openinbrowser: try: open_in_browser(filename) except: print("Failed to open output SVG in browser. SVG saved to:") print(filename)
def render(self, dwg: Drawing) -> Group: g = dwg.g() head = self.head.render(dwg) socket_relative_width = 1.2 socket_radius = (self.head_size * socket_relative_width, self.head_size*0.3) socket_relative_height = .3 socket_left = (-self.head_size * socket_relative_width, self.head_size * .5) socket_left_bottom = (socket_left[0], socket_left[1] + self.head_size * socket_relative_height) socket_right: Tuple[float, float] = (self.head_size * socket_relative_width, self.head_size * .5) socket_right_bottom: Tuple[float, float] = (socket_right[0], socket_right[1] + self.head_size * socket_relative_height) size_factor = self.head_size / 50.0 arm_length = 50 * size_factor arm_params = { "arm_length": arm_length, "arm_color": self.body_color, "hand_color": helper.colors.lighten_hex(self.body_color, 2), "thickness_shoulder": 30 * size_factor } arm_params.update(self.arm_params) for i in range(self.arm_count): left_arm = ArmWithHand(**arm_params) # type: ignore left_arm_g = left_arm.render(dwg) left_arm_x = socket_right_bottom[0] - left_arm.thickness_shoulder / 2 - (socket_right_bottom[0] - self.head_size * self.body_fatness) / (self.head_size * self.body_height) * i * left_arm.thickness_shoulder * 1.2 left_arm_g.translate(left_arm_x, socket_right_bottom[1] + left_arm.thickness_shoulder / 2 + i * left_arm.thickness_shoulder * .8) left_arm_g.rotate(self.body_left_arm_angle / (math.pi) * 180 + (i * 20)) g.add(left_arm_g) right_arm = ArmWithHand(reverse_shadow=True, **arm_params) # type: ignore right_arm_g = right_arm.render(dwg) right_arm_x = socket_left_bottom[0] + right_arm.thickness_shoulder / 2 + (-self.head_size * self.body_fatness - socket_left_bottom[0]) / (self.head_size * self.body_height) * i * right_arm.thickness_shoulder * 1.2 right_arm_g.translate(right_arm_x, socket_left_bottom[1] + right_arm.thickness_shoulder / 2 + i * right_arm.thickness_shoulder * .8) right_arm_g.rotate(-self.body_right_arm_angle / (math.pi) * 180 - (i * 20)) right_arm_g.scale(-1, 1) g.add(right_arm_g) leg_thickness_thigh = self.body_fatness * self.head_size leg_thickness_foot = leg_thickness_thigh * .7 leg_length = self.head_size * 1 boot_height = leg_length * .5 foot_length = leg_length left_leg = LegWithFoot(leg_length=leg_length, # type: ignore leg_color=self.body_color, thickness_thigh=leg_thickness_thigh, thickness_foot=leg_thickness_foot, foot_color=helper.colors.lighten_hex(self.body_color, 2), boot_height=boot_height, foot_length=foot_length, **self.leg_params) left_leg_g = left_leg.render(dwg) left_leg_g.translate(0, self.head_size * self.body_height) left_leg_g.rotate(-20) g.add(left_leg_g) right_leg = LegWithFoot(leg_length=leg_length, # type: ignore leg_color=self.body_color, thickness_thigh=leg_thickness_thigh, thickness_foot=leg_thickness_foot, foot_color=helper.colors.lighten_hex(self.body_color, 2), boot_height=boot_height, foot_length=foot_length, **self.leg_params) right_leg_g = right_leg.render(dwg) right_leg_g.translate(0, self.head_size * self.body_height) right_leg_g.rotate(20) right_leg_g.scale(-1, 1) g.add(right_leg_g) body = dwg.path(fill=self.body_color) body.push("M %f %f" % (socket_right_bottom[0], socket_right_bottom[1])) body.push("L %f %f" % (self.head_size * self.body_fatness, self.head_size * self.body_height)) body.push("L %f %f" % (self.head_size * (self.body_fatness - .2), self.head_size * (self.body_height + .2))) body.push("L %f %f" % (-self.head_size * (self.body_fatness - .2), self.head_size * (self.body_height + .2))) body.push("L %f %f" % (-self.head_size * self.body_fatness, self.head_size * self.body_height)) body.push("L %f %f" % (socket_left_bottom[0], socket_left_bottom[1])) g.add(body) socket_background_color = helper.colors.darken_hex(self.socket_color) socket_background = dwg.ellipse(fill=socket_background_color, center=(0, self.head_size * .5), r=socket_radius) socket_foreground = dwg.path(fill=self.socket_color) socket_foreground.push("M %f %f" % socket_left) socket_foreground.push("A %f %f 0 0 0 %f %f" % (socket_radius[0], socket_radius[1], socket_right[0], socket_right[1])) socket_foreground.push("l 0 %f" % (self.head_size * .3)) socket_foreground.push("A %f %f 0 0 1 %f %f" % (socket_radius[0], socket_radius[1], - self.head_size * socket_relative_width, self.head_size * .8)) g.add(socket_background) g.add(head) g.add(socket_foreground) dome = dwg.path(fill="white", fill_opacity=.3) dome.push("M %f %f" % socket_left) dome.push("C %f %f %f %f %f %f" % (-self.head_size * (socket_relative_width + 1), -self.head_size * 3, self.head_size * (socket_relative_width + 1), -self.head_size * 3, socket_right[0], socket_right[1])) dome.push("A %f %f 0 0 1 %f %f" % (socket_radius[0], socket_radius[1], socket_left[0], socket_left[1])) refl_mask = dwg.defs.add(dwg.mask((self.head_size * -1.5, self.head_size * -2.5), (self.head_size * 3, self.head_size * 5), id="%s-dome-refl-mask" % self.id)) refl_mask.add(dwg.rect((self.head_size * -1.5, self.head_size * -2.5), (self.head_size * 3, self.head_size * 5), fill="white")) refl_mask.add(dwg.circle((self.head_size * .3, -self.head_size * .25), r=self.head_size * 1.75, fill="black")) dome_reflection = dwg.path(fill="white", fill_opacity=.3, mask="url(#%s-dome-refl-mask)" % self.id) dome_reflection.push("M %f %f" % socket_left) dome_reflection.push("C %f %f %f %f %f %f" % (-self.head_size * (socket_relative_width + 1), -self.head_size * 3, self.head_size * (socket_relative_width + 1), -self.head_size * 3, socket_right[0], socket_right[1])) dome_reflection.push("A %f %f 0 0 1 %f %f" % (socket_radius[0], socket_radius[1], socket_left[0], socket_left[1])) dome_reflection.scale(.9) g.add(dome) g.add(dome_reflection) return g
def flatten_shape(i, all_paths, merge_paths): dwg = Drawing("merge_output%s.svg" % i, profile='tiny') def draw_line(start, end, offset=0.0): start += offset end += offset dwg.add( dwg.line(start=(start.real, start.imag), end=(end.real, end.imag), stroke_width=4, stroke=rgb(255, 0, 0))) dwg.add( dwg.path( **{ 'd': all_paths[i].d(), 'fill': "none", 'stroke-width': 4, 'stroke': rgb(0, 0, 0) })) dwg.add( dwg.path( **{ 'd': merge_paths[i].d(), 'fill': "none", 'stroke-width': 4, 'stroke': rgb(255, 0, 0) })) bbox = calc_overall_bbox(all_paths[i]) width, height = abs(bbox[1] - bbox[0]), abs(bbox[3] - bbox[2]) margin = 40 lower = min(bbox[2], bbox[3]) + height + margin left = min(bbox[0], bbox[1]) + margin def draw_marker(loc, col=rgb(255, 0, 0), offset=(left, lower)): dwg.add( dwg.circle(center=(loc.real + offset[0], loc.imag + offset[1]), r=4, fill=col)) max_axis = max(width, height) num_lines = 10 points = [merge_paths[i].point(j / num_lines) for j in range(num_lines)] + [merge_paths[i].point(1.0)] angles = [ asin((points[j + 1].imag - points[j].imag) / abs(points[j + 1] - points[j])) for j in range(num_lines) ] ends = [max_axis * (sin(angle) + cos(angle) * 1j) for angle in angles] intersection_clips = [] for j, end in enumerate(ends): end_point = end + points[j] intersections = other_paths[i].intersect( Line(start=points[j], end=end_point)) for intersection in intersections[0]: intersection_point = intersection[1].point(intersection[2]) target = merge_paths[i].length() * ( 1 - j / num_lines) + abs(intersection_point - points[j]) * 1j intersection_clips.append( PathClip(index=other_paths[i].index(intersection[1]), t=intersection[2], target=target)) if j % 10 == 0: draw_line(points[j], intersection_point) draw_marker(intersection_point, rgb(0, 255, 0), (0, 0)) break # make the flexed points by chopping the chunks of the other paths out, then # translating and rotating them such that their end points line up with the diff lines def transform_side(sides, targets, angle_offset=0): def angle(point1, point2): diff = point1 - point2 if diff.real == 0: return 90.0 return atan(diff.imag / diff.real) * 180.0 / pi # change this so that it has two targets transformed_side = Path(*sides) source_angle = angle(transformed_side.end, transformed_side.start) - \ angle(targets[0], targets[1]) transformed_side = transformed_side.rotated(-source_angle + angle_offset) source = transformed_side.end if angle_offset == 0 else transformed_side.start diff = targets[1] - source transformed_side = transformed_side.translated(diff) draw_marker(targets[0], rgb(0, 200, 200)) draw_marker(targets[1], rgb(0, 255, 255)) transformed_diff = abs(transformed_side.start - transformed_side.end) targets_diff = abs(targets[0] - targets[1]) if transformed_diff < targets_diff: transformed_side.insert( 0, Line(start=targets[0], end=transformed_side.start)) elif transformed_diff > targets_diff: # pop elements off until the transformed diff is smaller while transformed_diff > targets_diff: transformed_side.pop(0) transformed_diff = abs(transformed_side.start - transformed_side.end) print("path", transformed_side) print("path is longer", transformed_diff - targets_diff) return transformed_side start_index = 0 curr_t = 0 flexed_path = [] t_resolution = 0.01 if intersection_clips[0].index > intersection_clips[-1].index or \ (intersection_clips[0].index == intersection_clips[-1].index and intersection_clips[0].t > intersection_clips[-1].t): intersection_clips.reverse() # add the end of the shape to the intersection clips intersection_clips.append( PathClip(index=len(other_paths[i]) - 1, t=1.0, target=merge_paths[i].length())) last_target = 0 for clip in intersection_clips: sides = [] print("boundaries", start_index, clip.index, curr_t, clip.t) upper_t = clip.t if start_index == clip.index else 1.0 while start_index <= clip.index and curr_t < upper_t: curr_seg = other_paths[i][start_index] while curr_t < upper_t: max_t = curr_t + t_resolution if curr_t + t_resolution < clip.t else clip.t sides.append( Line(start=curr_seg.point(curr_t), end=curr_seg.point(max_t))) curr_t += t_resolution curr_t = upper_t if start_index != clip.index: curr_t = 0.0 if upper_t == 1.0: start_index += 1 upper_t = clip.t if start_index == clip.index else 1.0 if len(sides) != 0: flexed_path.append( transform_side(sides, [last_target, clip.target])) last_target = clip.target straight_path = [Line(start=0, end=merge_paths[i].length())] for p in flexed_path: p = p.translated(left + lower * 1j) dwg.add( dwg.path(d=p.d(), fill="none", stroke_width=4, stroke=rgb(255, 0, 0))) transformed_path = flexed_path + straight_path transformed_path = Path(*transformed_path).translated(left + lower * 1j) dwg.add( dwg.path(d=transformed_path.d(), fill="none", stroke_width=4, stroke=rgb(0, 0, 0))) bbox = calc_overall_bbox(list(all_paths[i]) + list(transformed_path)) width, height = abs(bbox[1] - bbox[0]), abs(bbox[3] - bbox[2]) dwg.viewbox(min(bbox[0], bbox[1]), min(bbox[2], bbox[3]), width, height) dwg.save() return flexed_path