def _extract_paths(group: svgelements.Group, recursive) -> _PathListType: """Extract everything from the provided SVG group.""" if recursive: everything = group.select() else: everything = group paths = [] for elem in everything: if hasattr(elem, "values") and elem.values.get("visibility", "") in ( "hidden", "collapse", ): continue if isinstance(elem, svgelements.Path): if len(elem) != 0: paths.append(elem) elif isinstance(elem, (svgelements.Polyline, svgelements.Polygon)): # Here we add a "fake" path containing just the Polyline/Polygon, # to be treated specifically by _convert_flattened_paths. path = [svgelements.Move(elem.points[0]), elem] if isinstance(elem, svgelements.Polygon): path.append(svgelements.Close(elem.points[-1], elem.points[0])) paths.append(path) elif isinstance(elem, svgelements.Shape): e = svgelements.Path(elem) e.reify( ) # In some cases the shape could not have reified, the path must. if len(e) != 0: paths.append(e) return paths
def _path(self): path = SE.Path(self._glyph) path *= f"scale({self.xscale * _scale()}, {self.yscale * _scale()})" # First rotate at 00, path *= f"rotate({self.rotate}deg)" # then move. path *= f"translate({self.x}, {self.y})" return path
def extract_shapely_polygons(svg_document, tolerance): height, width = get_sheet_dimensions(svg_document) s = io.StringIO(svg_document) svg_object = svgelements.SVG.parse(s, width=width, height=height) # Gather all paths in the document elements = [] paths = [] for element in svg_object.elements(): # Ignore hidden elements if 'hidden' in element.values and element.values[ 'visibility'] == 'hidden': continue # Treat paths as is, but convert other shapes into paths if isinstance(element, svgelements.Path): if len(element) != 0 and is_closed(element, tolerance): elements.append(element) paths.append(element) elif isinstance(element, svgelements.Shape): e = svgelements.Path(element) e.reify() if len(e) != 0 and is_closed(e, tolerance): elements.append(element) paths.append(e) # Extract outlines and holes from paths shapely_polygons = [] for path in paths: # Dilate the boundary, since the discrete path may actually unapproximate # and we want to ensure that the polygon always overapproximates the shape # # Note that the amounts are chosen because of the following reasons: # - We discretize at a spacing of (tolerance). This means that the discrete # polygon is wrong (missing points or containing extra points) at a # distance at most (tolerance/2) from the original shape # - By dilating the polygon by (1.5*tolerance), it is guaranteed to contain # all of the points in the original shape, plus at least (tolerance) # extra breathing room of buffering # - Simplifying by (tolerance) therefore results in a polygon that still # contains the original shape, and overapproximates it by points at a # distance at most 3*tolerance boundary = dilate(discretize_path(path.subpath(0), tolerance), 1.5 * tolerance).simplify(tolerance) # Erode the holes to ensure that the resulting polygon with holes is an # overapproximation of the original shape. Note that this might split a # hole into a MultiPolygon (handled below), or even make it empty hole_paths = list(path.as_subpaths())[1:] holes = list( erode(discretize_path(p, tolerance), 1.5 * tolerance).simplify(tolerance) for p in hole_paths if is_closed(p, tolerance)) shapely_polygons.append((boundary, holes)) assert (len(elements) == len(shapely_polygons)) return elements, shapely_polygons
def path_to_polylines(path_or_svgd, start=0j, tolerance=0.1): def subdivide_cubicBezier(cubic): for x, y in islice( aggsubdivision.bezier( (cubic.start.real, cubic.start.imag), (cubic.control1.real, cubic.control1.imag), (cubic.control2.real, cubic.control2.imag), (cubic.end.real, cubic.end.imag), distance_tolerance=tolerance), 1, None): yield complex(x, y) def subpath_to_polyline(subpath): for seg in subpath: if isinstance(seg, svgelements.Move): yield complex(*seg.end) elif isinstance(seg, svgelements.Line): yield complex(*seg.end) elif isinstance(seg, svgelements.CubicBezier): yield from subdivide_cubicBezier(seg) elif isinstance(seg, svgelements.QuadraticBezier): cubic = svgelements.CubicBezier( seg.start, 1 / 3 * seg.start + 2 / 3 * seg.control, 2 / 3 * seg.control + 1 / 3 * seg.end, seg.end) yield from subdivide_cubicBezier(cubic) elif isinstance(seg, svgelements.Arc): for cubic in seg.as_cubic_curves(): yield from subdivide_cubicBezier(cubic) elif isinstance(seg, svgelements.Close): yield complex(*seg.end) else: logging.warn('unimplemented segement type %s', type(seg)) if isinstance(path_or_svgd, svgelements.Path): path = path_or_svgd else: path = svgelements.Path() path.append(svgelements.Move(start)) path.parse(path_or_svgd) path.pop(0) for subpath in path.as_subpaths(): if subpath: yield list(subpath_to_polyline(subpath))
def find_paths(svgfn, scale=1): svg = ET.parse(svgfn).getroot() try: x0, y0, w, h = map(float, svg.attrib['viewBox'].split(' ')) sx = Quantity(svg.attrib['width']).m_as('mm') / w sy = Quantity(svg.attrib['height']).m_as('mm') / h xform0 = 'scale(%s %s)' % (sx * scale, sy * scale) except KeyError: logging.warn('could not determine SVG units, assuming mm') xform0 = 'scale(%s %s)' % (scale, scale) for e, ancestors in walk_svg(svg): tag = simple_tag(e) if tag == 'path': path = svgelements.Path(e.get('d', '')) transform_str = ' '.join( e2.get('transform', '') for e2 in ancestors) transform = svgelements.Matrix(xform0 + transform_str) transformed_path = path * transform transformed_path.reify() yield transformed_path
def install_font1(path, overwrite=False): name, ext = os.path.splitext(os.path.basename(path)) if os.path.exists(f"./fonts/json/{name}.json") and not overwrite: raise FileExistsError(f"{name} is already installed.") else: D = {} D[name] = {} if ext == ".svg": with open(f"./fonts/json/{name}.json", "w") as file_: font = ET.parse(path).getroot().find("ns:defs", _SVGNS).find( "ns:font", _SVGNS) for glyph in font.findall("ns:glyph", _SVGNS): try: path = SE.Path(glyph.attrib["d"], transform="scale(1 -1)") # .scaled(sx=1, sy=-1) # svgpathtools' scaled() method has a bug which deforms shapes. It offers however good bbox support. # svgelements has unreliable bbox functionality, but transformations seem to be more safe than in pathtools. # Bypass: apply transformations in svgelements and pass the d() to pathtools to get bboxes when needed. min_x, min_y, max_x, max_y = path.bbox() D[name][glyph.get("glyph-name")] = { "d": path.d(), "left": min_x, "right": max_x, "top": min_y, "bottom": max_y, "width": max_x - min_x, "height": max_y - min_y } # D[name][glyph.get("glyph-name")] = glyph.attrib["d"] except KeyError: pass json.dump(D[name], file_, indent=2) del path del glyph else: raise NotImplementedError("Non-svg fonts are not supported!")
def get_boundary(path): return svgelements.Path(svgelements.Path(path).subpath(0))
def discretize_path(path, spacing): path = svgelements.Path(path) # Convert Subpath to Path length = path.length() n_points = max(3, math.ceil(length / spacing)) points = [path.point(i * (1 / n_points)) for i in range(n_points)] return to_shapely_polygon(points)