Пример #1
0
    def __init__(
        self,
        svg_path=None,
        image_savedir=None,
        plot_id=None,
        cam=None,
        camera_index=None,
    ):
        if plot_id == None:
            plot_id = fn.get_current_plot_id()
        self.plot_id = plot_id

        if svg_path == None:
            svg_path = Path(
                gp.SVG_SAVEDIR).joinpath(plot_id).with_suffix('.svg')
        self.svg_path = svg_path
        self.ad = axidraw.AxiDraw()
        self.ad.plot_setup(self.svg_path)
        self.ad.options.mode = "layers"
        self.ad.options.units = 2
        self.ad.update()
        self.doc = vpype.read_multilayer_svg(self.svg_path, 0.1)
        self.image_savedir = image_savedir
        self.camera_index = camera_index
        self.cam = cam
Пример #2
0
    def load_svg(self) -> None:
        self.document = vp.read_multilayer_svg(str(self.path),
                                               quantization=0.1)
        self.layer_visibility.clear()
        self.layer_list.populate(self.document)

        # create page size label
        page_size = self.document.page_size
        if page_size[0] < page_size[1]:
            landscape = False
        else:
            page_size = tuple(reversed(page_size))
            landscape = True

        format_name = ""
        for name, sz in vp.PAGE_SIZES.items():
            if math.isclose(sz[0], page_size[0],
                            abs_tol=0.01) and math.isclose(
                                sz[1], page_size[1], abs_tol=0.01):
                format_name = name
                break
        s = (f"{self.document.page_size[0] / 96 * 25.4:.1f}x"
             f"{self.document.page_size[1] / 96 * 25.4:.1f}mm")
        if format_name != "":
            s += f" ({format_name} {'landscape' if landscape else'portrait'})"
        self.page_format = s
Пример #3
0
def test_viewer_scale_origin(assert_image_similarity):
    doc = vp.read_multilayer_svg(
        str(TEST_FILE_DIRECTORY / "issue_124/plotter.svg"), 0.4)
    assert_image_similarity(
        render_image(doc,
                     view_mode=ViewMode.OUTLINE,
                     origin=(600, 400),
                     scale=4))
Пример #4
0
def test_viewer_zoom_scale(assert_image_similarity):
    doc = vp.read_multilayer_svg(str(TEST_FILE_DIRECTORY / "issue_124/plotter.svg"), 0.4)
    renderer = ImageRenderer((1024, 1024))
    renderer.engine.document = doc
    renderer.engine.fit_to_viewport()
    renderer.engine.show_rulers = False
    renderer.engine.zoom(2, 500, 500)
    renderer.engine.pan(160, 250)
    assert_image_similarity(renderer.render())
Пример #5
0
def test_benchmark_linemerge(benchmark):
    doc = vp.read_multilayer_svg(
        str(TEST_FILE_DIRECTORY / "benchmark/multi_skull.svg"), 0.1)

    cnt = len(doc.layers[1])
    doc = benchmark(vpype_cli.execute, "linemerge", doc)

    assert len(doc.layers[1]) > 0
    assert cnt > len(doc.layers[1])
Пример #6
0
def test_viewer_debug(assert_image_similarity):
    doc = vp.read_multilayer_svg(str(TEST_FILE_DIRECTORY / "issue_124/plotter.svg"), 0.4)
    renderer = ImageRenderer((1024, 1024))
    renderer.engine.document = doc
    renderer.engine.origin = (600, 400)
    renderer.engine.scale = 8
    renderer.engine.view_mode = ViewMode.PREVIEW
    renderer.engine.debug = True
    renderer.engine.show_rulers = False
    assert_image_similarity(renderer.render())
Пример #7
0
def test_benchmark_linesort(benchmark):
    doc = vp.read_multilayer_svg(
        str(TEST_FILE_DIRECTORY / "benchmark/7k_lines.svg"), 0.1)

    pen_up_length, _, _ = doc.layers[1].pen_up_length()
    cnt = len(doc.layers[1])
    doc = benchmark(vpype_cli.execute, "linesort", doc)

    assert pen_up_length > doc.layers[1].pen_up_length()[0]
    assert cnt == len(doc.layers[1])
Пример #8
0
def read(
    vector_data: VectorData,
    file,
    single_layer: bool,
    layer: Optional[int],
    quantization: float,
    no_simplify: bool,
) -> VectorData:
    """Extract geometries from a SVG file.

    By default, the `read` command attempts to preserve the layer structure of the SVG. In this
    context, top-level groups (<svg:g>) are each considered a layer. If any, all non-group,
    top-level SVG elements are imported into layer 1.

    The following logic is used to determine in which layer each SVG top-level group is
    imported:

        - If a `inkscape:label` attribute is present and contains digit characters, it is
    stripped of non-digit characters the resulting number is used as target layer. If the
    resulting number is 0, layer 1 is used instead.

        - If the previous step fails, the same logic is applied to the `id` attribute.

        - If both previous steps fail, the target layer matches the top-level group's order of
    appearance.

    Using `--single-layer`, the `read` command operates in single-layer mode. In this mode, all
    geometries are in a single layer regardless of the group structure. The current target
    layer is used default and can be specified with the `--layer` option.

    This command only extracts path elements as well as primitives (rectangles, ellipses,
    lines, polylines, polygons). Other elements such as text and bitmap images are discarded,
    and so is all formatting.

    All curved primitives (e.g. bezier path, ellipses, etc.) are linearized and approximated by
    polylines. The quantization length controls the maximum length of individual segments.

    By default, an implicit line simplification with tolerance set to quantization is executed
    (see `linesimplify` command). This behaviour can be disabled with the `--no-simplify` flag.

    Examples:

        Multi-layer import:

            vpype read input_file.svg [...]

        Single-layer import:

            vpype read --single-layer input_file.svg [...]

        Single-layer import with target layer:

            vpype read --single-layer --layer 3 input_file.svg [...]

        Multi-layer import with specified quantization and line simplification disabled:

            vpype read --quantization 0.01mm --no-simplify input_file.svg [...]
    """

    if single_layer:
        vector_data.add(
            read_svg(file, quantization=quantization,
                     simplify=not no_simplify),
            single_to_layer_id(layer, vector_data),
        )
    else:
        if layer is not None:
            logging.warning(
                "read: target layer is ignored in multi-layer mode")
        vector_data.extend(
            read_multilayer_svg(file,
                                quantization=quantization,
                                simplify=not no_simplify))

    return vector_data
Пример #9
0
 def load_svg(self, path: str):
     # TODO: make quantization a parameters
     vd = vpype.read_multilayer_svg(path, 0.05)
     self.set_vector_data(vd)
Пример #10
0
def test_viewer(assert_image_similarity, file, render_kwargs):
    doc = vp.read_multilayer_svg(str(TEST_FILE_DIRECTORY / file), 0.4)

    # noinspection PyArgumentList
    assert_image_similarity(render_image(doc, (1024, 1024), **render_kwargs))
Пример #11
0
def doc_for_render():
    return vp.read_multilayer_svg(
        str(TEST_FILE_DIRECTORY / "benchmark/7k_lines.svg"), 0.1)
Пример #12
0
    def effect(self):
        lc = vpype.LineCollection() # create a new array of LineStrings consisting of Points. We convert selected paths to polylines and grab their points
        elementsToWork = [] # we make an array of all collected nodes to get the boundingbox of that array. We need it to place the vpype converted stuff to the correct XY coordinates
          
        applyTransformAvailable = False
        
        # at first we apply external extension
        try:
            sys.path.append("..") # add parent directory to path to allow importing applytransform (vpype extension is encapsulated in sub directory)
            import applytransform
            applyTransformAvailable = True
        except Exception as e:
            # inkex.utils.debug(e)
            inkex.utils.debug("Calling 'Apply Transformations' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping this step")

        def flatten(node):
            path = node.path.to_superpath()
            bezier.cspsubdiv(path, self.options.flatness)
            newpath = []
            for subpath in path:
                first = True
                for csp in subpath:
                    cmd = 'L'
                    if first:
                        cmd = 'M'
                    first = False
                    newpath.append([cmd, [csp[1][0], csp[1][1]]])
            node.path = newpath

        # flatten the node's path to linearize, split up the path to it's subpaths (break apart) and add all points to the vpype lines collection
        def convertPath(node, nodes = None):
            if nodes is None:
                nodes = []
            if node.tag == inkex.addNS('path','svg'):
                nodes.append(node)
                if self.options.flattenbezier is True:
                    flatten(node)

                raw = node.path.to_arrays()
                subPaths, prev = [], 0
                for i in range(len(raw)): # Breaks compound paths into simple paths
                    if raw[i][0] == 'M' and i != 0:
                        subPaths.append(raw[prev:i])
                        prev = i
                subPaths.append(raw[prev:])
                for subPath in subPaths:
                    points = []
                    for csp in subPath:
                        if len(csp[1]) > 0: #we need exactly two points per straight line segment
                            points.append(Point(round(csp[1][0], self.options.decimals), round(csp[1][1], self.options.decimals)))
                    if  subPath[-1][0] == 'Z' or subPath[0][1] == subPath[-1][1]:  #check if path is closed by Z or first pont == last point
                        points.append(Point(round(subPath[0][1][0], self.options.decimals), round(subPath[0][1][1], self.options.decimals))) #if closed, we add the first point again
                    lc.append(LineString(points))
                      
            children = node.getchildren()
            if children is not None: 
                for child in children:
                    convertPath(child, nodes)
            return nodes

        doc = None #create a vpype document
        
        '''
        if 'paths' we process paths only. Objects like rectangles or strokes like polygon have to be converted before accessing them
        if 'layers' we can process all layers in the complete document
        '''
        if self.options.input_handling == "paths":
            # getting the bounding box of the current selection. We use to calculate the offset XY from top-left corner of the canvas. This helps us placing back the elements
            input_bbox = None
            if self.options.apply_transformations is True and applyTransformAvailable is True:
                '''
                we need to apply transfoms to the complete document even if there are only some single paths selected. 
                If we apply it to selected nodes only the parent groups still might contain transforms. 
                This messes with the coordinates and creates hardly controllable behaviour
                '''
                applytransform.ApplyTransform().recursiveFuseTransform(self.document.getroot())
            if len(self.svg.selected) == 0:
                elementsToWork = convertPath(self.document.getroot())
                for element in elementsToWork:
                    input_bbox += element.bounding_box()      
            else:
                elementsToWork = None
                for element in self.svg.selected.values():
                    elementsToWork = convertPath(element, elementsToWork)
                #input_bbox = inkex.elements._selected.ElementList.bounding_box(self.svg.selected) # get BoundingBox for selection
                input_bbox = self.svg.selection.bounding_box() # get BoundingBox for selection
            if len(lc) == 0:
                inkex.errormsg('Selection appears to be empty or does not contain any valid svg:path nodes. Try to cast your objects to paths using CTRL + SHIFT + C or strokes to paths using CTRL + ALT+ C')
                return  
            # find the first object in selection which has a style attribute (skips groups and other things which have no style)
            firstElementStyle = None
            for element in elementsToWork:
                if element.attrib.has_key('style'):
                    firstElementStyle = element.get('style')
            doc = vpype.Document(page_size=(input_bbox.width + input_bbox.left, input_bbox.height + input_bbox.top)) #create new vpype document     
            doc.add(lc, layer_id=None) # we add the lineCollection (converted selection) to the vpype document
            
        elif self.options.input_handling == "layers":
            doc = vpype.read_multilayer_svg(self.options.input_file, quantization = self.options.flatness, crop = False, simplify = self.options.simplify, parallel = self.options.parallel, default_width = self.document.getroot().get('width'), default_height = self.document.getroot().get('height'))

            for element in self.document.getroot().xpath("//svg:g", namespaces=inkex.NSS): #all groups/layers
                elementsToWork.append(element)

        tooling_length_before = doc.length()
        traveling_length_before = doc.pen_up_length()
        
        # build and execute the conversion command
        # the following code block is not intended to sum up the commands to build a series (pipe) of commands!
        ##########################################
        
        # Line Sorting
        if self.options.linesort is True:
            command = "linesort "
            if self.options.linesort_no_flip is True:
                command += " --no-flip"

        # Line Merging
        if self.options.linemerge is True:     
            command = "linemerge --tolerance " + str(self.options.linemerge_tolerance)
            if self.options.linemerge_no_flip is True:
                command += " --no-flip"
 
        # Trimming
        if self.options.trim is True:     
            command = "trim " + str(self.options.trim_x_margin) + " " + str(self.options.trim_y_margin)
 
        # Relooping
        if self.options.reloop is True:     
            command = "reloop --tolerance " + str(self.options.reloop_tolerance)
 
        # Multipass
        if self.options.multipass is True:     
            command = "multipass --count " + str(self.options.multipass_count)
 
        # Filter
        if self.options.filter is True:     
            command = "filter --tolerance " + str(self.options.filter_tolerance)
            if self.options.filter_min_length_enabled is True:
                command += " --min-length " + str(self.options.filter_min_length)
            if self.options.filter_max_length_enabled is True:
                command += " --max-length " + str(self.options.filter_max_length)
            if self.options.filter_closed is True and self.options.filter_not_closed is False:
                command += " --closed"
            if self.options.filter_not_closed is True and self.options.filter_closed is False:
                command += " --not-closed"
            if self.options.filter_closed is False and \
                self.options.filter_not_closed is False and \
                self.options.filter_min_length_enabled is False and \
                self.options.filter_max_length_enabled is False:
                inkex.errormsg('No filters to apply. Please select at least one filter.')
                return

        # Plugin Occult
        if self.options.plugin_occult is True:     
            command = "occult --tolerance " + str(self.options.plugin_occult_tolerance)
            if self.options.plugin_occult_keepseparatelayer is True:
                command += " --keep-occulted"

        # Split All
        if self.options.splitall is True:     
            command = " splitall"

        # Free Mode
        if self.options.freemode is True:
            command = ""
            if self.options.freemode_cmd1_enabled is True:
                command += " " + self.options.freemode_cmd1.strip()
            if self.options.freemode_cmd2_enabled is True:
                command += " " + self.options.freemode_cmd2.strip()
            if self.options.freemode_cmd3_enabled is True:
                command += " " + self.options.freemode_cmd3.strip()
            if self.options.freemode_cmd4_enabled is True:
                command += " " + self.options.freemode_cmd4.strip()
            if self.options.freemode_cmd5_enabled is True:
                command += " " + self.options.freemode_cmd5.strip()
            if self.options.freemode_cmd1_enabled is False and \
                self.options.freemode_cmd2_enabled is False and \
                self.options.freemode_cmd3_enabled is False and \
                self.options.freemode_cmd4_enabled is False and \
                self.options.freemode_cmd5_enabled is False:
                inkex.utils.debug("Warning: empty vpype pipeline. With this you are just getting read-write layerset/lineset.")
            else:
                if self.options.freemode_show_cmd is True:
                    inkex.utils.debug("Your command pipe will be the following:")
                    inkex.utils.debug(command)

        # inkex.utils.debug(command)
        try:
            doc = execute(command, doc)
        except Exception as e:
            inkex.utils.debug("Error in vpype:" + str(e))
            return

        ##########################################
        
        tooling_length_after = doc.length()
        traveling_length_after = doc.pen_up_length()        
        if tooling_length_before > 0:
            tooling_length_saving = (1.0 - tooling_length_after / tooling_length_before) * 100.0
        else:
            tooling_length_saving = 0.0            
        if traveling_length_before > 0:
            traveling_length_saving = (1.0 - traveling_length_after / traveling_length_before) * 100.0
        else:
            traveling_length_saving = 0.0  
        if self.options.output_stats is True:
            inkex.utils.debug('Total tooling length before vpype conversion: '   + str('{:0.2f}'.format(tooling_length_before))   + ' mm')
            inkex.utils.debug('Total traveling length before vpype conversion: ' + str('{:0.2f}'.format(traveling_length_before)) + ' mm')
            inkex.utils.debug('Total tooling length after vpype conversion: '    + str('{:0.2f}'.format(tooling_length_after))    + ' mm')
            inkex.utils.debug('Total traveling length after vpype conversion: '  + str('{:0.2f}'.format(traveling_length_after))  + ' mm')
            inkex.utils.debug('Total tooling length optimized: '   + str('{:0.2f}'.format(tooling_length_saving))   + ' %')
            inkex.utils.debug('Total traveling length optimized: ' + str('{:0.2f}'.format(traveling_length_saving)) + ' %')
         
        if tooling_length_after == 0:
            inkex.errormsg('No lines left after vpype conversion. Conversion result is empty. Cannot continue')
            return
         
        # show the vpype document visually
        if self.options.output_show:
            warnings.filterwarnings("ignore") # workaround to suppress annoying DeprecationWarning
            # vpype_viewer.show(doc, view_mode=ViewMode.PREVIEW, show_pen_up=self.options.output_trajectories, show_points=self.options.output_show_points, pen_width=0.1, pen_opacity=1.0, argv=None)
            vpype_viewer.show(doc, view_mode=ViewMode.PREVIEW, show_pen_up=self.options.output_trajectories, show_points=self.options.output_show_points, argv=None) # https://vpype.readthedocs.io/en/stable/api/vpype_viewer.ViewMode.html
            warnings.filterwarnings("default") # reset warning filter
            exit(0) #we leave the code loop because we only want to preview. We don't want to import the geometry
          
        # save the vpype document to new svg file and close it afterwards
        output_file = self.options.input_file + ".vpype.svg"
        output_fileIO = open(output_file, "w", encoding="utf-8")
        #vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer', single_path = True)       
        vpype.write_svg(output_fileIO, doc, page_size=None, center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer')       
        #vpype.write_svg(output_fileIO, doc, page_size=(self.svg.unittouu(self.document.getroot().get('width')), self.svg.unittouu(self.document.getroot().get('height'))), center=False, source_string='', layer_label_format='%d', show_pen_up=self.options.output_trajectories, color_mode='layer')       
        output_fileIO.close()
        
        # convert vpype polylines/lines/polygons to regular paths again. We need to use "--with-gui" to respond to "WARNING: ignoring verb FileSave - GUI required for this verb."
        if self.options.strokes_to_paths is True:
            cli_output = inkscape(output_file, "--with-gui", actions="EditSelectAllInAllLayers;EditUnlinkClone;ObjectToPath;FileSave;FileQuit")
            if len(cli_output) > 0:
                self.debug(_("Inkscape returned the following output when trying to run the vpype object to path back-conversion:"))
                self.debug(cli_output)
        
        # this does not work because line, polyline and polygon have no base class to execute replace_with
        #if self.options.strokes_to_paths is True:
        #    for lineLayer in lineLayers:
        #        for element in lineLayer:
        #                element.replace_with(element.to_path_element())
               
        # parse the SVG file
        try:
            stream = open(output_file, 'r')
        except FileNotFoundError as e:
            inkex.utils.debug("There was no SVG output generated by vpype. Cannot continue")
            exit(1)
        p = etree.XMLParser(huge_tree=True)
        import_doc = etree.parse(stream, parser=etree.XMLParser(huge_tree=True))
        stream.close()
          
        # handle pen_up trajectories (travel lines)
        trajectoriesLayer = import_doc.getroot().xpath("//svg:g[@id='pen_up_trajectories']", namespaces=inkex.NSS)
        if self.options.output_trajectories is True:
            if len(trajectoriesLayer) > 0:
                trajectoriesLayer[0].set('style', 'stroke:#0000ff;stroke-width:{:0.2f}px;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill:none'.format(self.options.trajectories_stroke_width))
                trajectoriesLayer[0].attrib.pop('stroke') # remove unneccesary stroke attribute
                trajectoriesLayer[0].attrib.pop('fill') # remove unneccesary fill attribute
        else:
            if len(trajectoriesLayer) > 0:
                trajectoriesLayer[0].delete()
   
        lineLayers = import_doc.getroot().xpath("//svg:g[not(@id='pen_up_trajectories')]", namespaces=inkex.NSS) #all layer except the pen_up trajectories layer
        if self.options.use_style_of_first_element is True and self.options.input_handling == "paths" and firstElementStyle is not None:
            
            # if we remove the fill property and use "Use style of first element in layer" the conversion will just crash with an unknown reason
            #declarations = firstElementStyle.split(';')
            #for i, decl in enumerate(declarations):
            #    parts = decl.split(':', 2)
            #    if len(parts) == 2:
            #        (prop, val) = parts
            #        prop = prop.strip().lower()
            #        #if prop == 'fill':
            #        #   declarations[i] = prop + ':none'   
            for lineLayer in lineLayers:
                #lineLayer.set('style', ';'.join(declarations))
                lineLayer.set('style', firstElementStyle)
                lineLayer.attrib.pop('stroke') # remove unneccesary stroke attribute
                lineLayer.attrib.pop('fill') # remove unneccesary fill attribute

        else:
            for lineLayer in lineLayers:          
                if lineLayer.attrib.has_key('stroke'):
                    color = lineLayer.get('stroke')
                    lineLayer.set('style', 'stroke:' + color + ';stroke-width:{:0.2f}px;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill:none'.format(self.options.lines_stroke_width))
                    lineLayer.attrib.pop('stroke') # remove unneccesary stroke attribute
                    lineLayer.attrib.pop('fill') # remove unneccesary fill attribute

        import_viewBox = import_doc.getroot().get('viewBox').split(" ")
        self_viewBox = self.document.getroot().get('viewBox').split(" ")
        scaleX = self.svg.unittouu(self_viewBox[2]) / self.svg.unittouu(import_viewBox[2])
        scaleY = self.svg.unittouu(self_viewBox[3]) / self.svg.unittouu(import_viewBox[3])

        for element in import_doc.getroot().iter("{http://www.w3.org/2000/svg}g"):
            self.document.getroot().append(element)
            if self.options.input_handling == "layers":
                element.set('transform', 'scale(' + str(scaleX) + ',' + str(scaleY) + ')') #imported groups need to be transformed. Or they have wrong size. Reason: different viewBox sizes/units in namedview definitions
                if self.options.apply_transformations is True and applyTransformAvailable is True: #we apply the transforms directly after adding them
                    applytransform.ApplyTransform().recursiveFuseTransform(element) 

        # Delete the temporary file again because we do not need it anymore
        if os.path.exists(output_file):
            os.remove(output_file)
            
        # Remove selection objects to do a real replace with new objects from vpype document
        if self.options.keep_objects is False:
            for element in elementsToWork:
                element.delete()
Пример #13
0
def read(
    document: Document,
    file,
    single_layer: bool,
    layer: Optional[int],
    quantization: float,
    simplify: bool,
    parallel: bool,
    no_crop: bool,
    display_size: Tuple[float, float],
    display_landscape: bool,
) -> Document:
    """Extract geometries from a SVG file.

    By default, the `read` command attempts to preserve the layer structure of the SVG. In this
    context, top-level groups (<svg:g>) are each considered a layer. If any, all non-group,
    top-level SVG elements are imported into layer 1.

    The following logic is used to determine in which layer each SVG top-level group is
    imported:

        - If a `inkscape:label` attribute is present and contains digit characters, it is \
stripped of non-digit characters the resulting number is used as target layer. If the \
resulting number is 0, layer 1 is used instead.

        - If the previous step fails, the same logic is applied to the `id` attribute.

        - If both previous steps fail, the target layer matches the top-level group's order \
of appearance.

    Using `--single-layer`, the `read` command operates in single-layer mode. In this mode, \
all geometries are in a single layer regardless of the group structure. The current target \
layer is used default and can be specified with the `--layer` option.

    This command only extracts path elements as well as primitives (rectangles, ellipses,
    lines, polylines, polygons). Other elements such as text and bitmap images are discarded,
    and so is all formatting.

    All curved primitives (e.g. bezier path, ellipses, etc.) are linearized and approximated by
    polylines. The quantization length controls the maximum length of individual segments.

    Optionally, a line simplification with tolerance set to quantization can be applied on the
    SVG's curved element (e.g. circles, ellipses, arcs, bezier curves, etc.). This is enabled
    with the `--simplify` flag. This process reduces significantly the number of segments used
    to approximate the curve while still guaranteeing an accurate conversion, but may increase
    the execution time of this command.

    The `--parallel` option enables multiprocessing for the SVG conversion. This is recommended
    ONLY when using `--simplify` on large SVG files with many curved elements.

    By default, the geometries are cropped to the SVG boundaries defined by its width and
    length attributes. The crop operation can be disabled with the `--no-crop` option.

    In general, SVG boundaries are determined by the `width` and `height` of the top-level
    <svg> tag. However, the some SVG may have their width and/or height specified as percent
    value or even miss them altogether (in which case they are assumed to be set to 100%). In
    these cases, vpype considers by default that 100% corresponds to a A4 page in portrait
    orientation. The options `--display-size FORMAT` and `--display-landscape` can be used
    to specify a different format.

    Examples:

        Multi-layer import:

            vpype read input_file.svg [...]

        Single-layer import:

            vpype read --single-layer input_file.svg [...]

        Single-layer import with target layer:

            vpype read --single-layer --layer 3 input_file.svg [...]

        Multi-layer import with specified quantization and line simplification enabled:

            vpype read --quantization 0.01mm --simplify input_file.svg [...]

        Multi-layer import with cropping disabled:

            vpype read --no-crop input_file.svg [...]
    """

    width, height = display_size
    if display_landscape:
        width, height = height, width

    if single_layer:
        lc, width, height = read_svg(
            file,
            quantization=quantization,
            crop=not no_crop,
            simplify=simplify,
            parallel=parallel,
            default_width=width,
            default_height=height,
        )

        document.add(lc, single_to_layer_id(layer, document))
        document.extend_page_size((width, height))
    else:
        if layer is not None:
            logging.warning("read: target layer is ignored in multi-layer mode")
        document.extend(
            read_multilayer_svg(
                file,
                quantization=quantization,
                crop=not no_crop,
                simplify=simplify,
                parallel=parallel,
                default_width=width,
                default_height=height,
            )
        )

    return document