Exemplo n.º 1
0
    def __init__(self, tolerance, target_size):
        # parsed path data, paths by color
        # {'#ff0000': [[[x,y], [x,y], ...], [], ..], '#0000ff':[]}
        # Each path is a list of vertices which is a list of two floats.
        self.boundarys = {}

        # the conversion factor to physical dimensions
        # applied to all coordinates in the SVG
        self.px2mm = None

        # what the svg size (typically page dimensions) should be mapped to
        self._target_size = target_size

        # tolerance settings, used in tessalation, path simplification, etc
        self.tolerance = tolerance
        self.tolerance2 = tolerance**2
        self.tolerance2_half = (0.5 * tolerance)**2
        self.tolerance2_px = None

        # init helper object for tag reading
        self._tagReader = SVGTagReader(self)

        # lasersaur cut setting from SVG file
        # list of triplets ... [(pass#, key, value), ...]
        # pass# designates the pass this lasertag controls
        # key is the kind of setting (one of: intensity, feedrate, color)
        # value is the actual value to use
        self.lasertags = []
Exemplo n.º 2
0
    def __init__(self, tolerance, target_size):
        # parsed path data, paths by color
        # {'#ff0000': [[[x,y], [x,y], ...], [], ..], '#0000ff':[]}
        # Each path is a list of vertices which is a list of two floats.
        self.boundarys = {}

        # the conversion factor to physical dimensions
        # applied to all coordinates in the SVG
        self.px2mm = None

        # what the svg size (typically page dimensions) should be mapped to
        self._target_size = target_size

        # tolerance settings, used in tessalation, path simplification, etc
        self.tolerance = tolerance
        self.tolerance2 = tolerance**2
        self.tolerance2_half = (0.5*tolerance)**2
        self.tolerance2_px = None

        # init helper object for tag reading
        self._tagReader = SVGTagReader(self)

        # lasersaur cut setting from SVG file
        # list of triplets ... [(pass#, key, value), ...]
        # pass# designates the pass this lasertag controls
        # key is the kind of setting (one of: intensity, feedrate, color)
        # value is the actual value to use
        self.lasertags = []
Exemplo n.º 3
0
class SVGReader:
    """SVG parser.

    Usage:
    reader = SVGReader(0.08, [1220,610])
    boundarys = reader.parse(open('filename').read())
    """

    def __init__(self, tolerance, target_size):
        # parsed path data, paths by color
        # {'#ff0000': [[[x,y], [x,y], ...], [], ..], '#0000ff':[]}
        # Each path is a list of vertices which is a list of two floats.
        self.boundarys = {}

        # the conversion factor to physical dimensions
        # applied to all coordinates in the SVG
        self.px2mm = None

        # what the svg size (typically page dimensions) should be mapped to
        self._target_size = target_size

        # tolerance settings, used in tessalation, path simplification, etc
        self.tolerance = tolerance
        self.tolerance2 = tolerance**2
        self.tolerance2_half = (0.5*tolerance)**2
        self.tolerance2_px = None

        # init helper object for tag reading
        self._tagReader = SVGTagReader(self)

        # lasersaur cut setting from SVG file
        # list of triplets ... [(pass#, key, value), ...]
        # pass# designates the pass this lasertag controls
        # key is the kind of setting (one of: intensity, feedrate, color)
        # value is the actual value to use
        self.lasertags = []

        # # tags that should not be further traversed
        # self.ignore_tags = {'defs':None, 'pattern':None, 'clipPath':None}


    def parse(self, svgstring, force_dpi=None):
        """ Parse a SVG document.

        This traverses through the document tree and collects all path
        data and converts it to polylines of the requested tolerance.

        Path data is returned as paths by color:
        {'#ff0000': [[path0, path1, ..], [path0, ..], ..]}
        Each path is a list of vertices which is a list of two floats.

        Determining Physical Dimensions
        -------------------------------
        SVG files may use physical units (mm, in) or screen units (px).
        For obvious reason former are preferred as these take out any
        guess-work of how to interpret any coordinates.

        A good SVG authoring app writes physical dimensions to file like this:
        - the svg tag has a width, height, viewBox attribute
        - width and height contains the page dimensions and unit
        - viewBox defines a rectangle with (x, y, width, height)
        - width/viewBox:width is the factor that needs to be applied to
          any (unit-less) coordinates in the file
        - x,y is a translation that needs to be applied to any coordinates

        One issue with svg documents is that they are not always clear on
        the physical dimensions. Often they lack or use px units in the
        width/height attributes (no units implies px units in the SVG
        standard). For example, it's possible to encounter px
        units in the file even when the authoring app interprets these
        as physical units (e.g mm). This means there is an implied DPI
        conversion in the app that we need to guess/know.

        The following strategy is used to get physical dimensions:

        1. from argument (force_dpi)
        2. from units of svg width/height and viewBox
        3. from hints of (known) originating apps
        4. from ratio of page and target size
        5. defaults to 90 DPI
        """
        self.px2mm = None
        self.boundarys = {}

        vb_x = None
        vb_y = None
        vb_w = None
        vb_h = None

        # parse xml
        svgRootElement = ET.fromstring(svgstring)
        tagName = self._tagReader._get_tag(svgRootElement)

        if tagName != 'svg':
            log.error("Invalid file, no 'svg' tag found.")
            return self.boundarys

        # 1. Get px2mm from argument
        if force_dpi is not None:
            self.px2mm = 25.4/force_dpi
            log.info("SVG import forced to %s dpi." % (force_dpi))

        # Get width, height, viewBox for further processing
        if not self.px2mm:
            width = None
            height = None
            unit = ''

            # get width, height, unit
            width_str = svgRootElement.attrib.get('width')
            height_str = svgRootElement.attrib.get('height')
            if width_str and height_str:
                width, width_unit = parseScalar(width_str)
                height, height_unit = parseScalar(height_str)
                if width_unit != height_unit:
                    log.error("Conflicting units found.")
                unit = width_unit
                log.info("SVG w,h (unit) is %s,%s (%s)." % (width, height, unit))

            # get viewBox
            # http://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute
            vb = svgRootElement.attrib.get('viewBox')
            if vb:
                vb_x, vb_y, vb_w, vb_h = parseFloats(vb)
                log.info("SVG viewBox (%s,%s,%s,%s)." % (vb_x, vb_y, vb_w, vb_h))

        # 2. Get px2mm from width, height, viewBox
        if not self.px2mm:
            if (width and height) or vb:
                if not (width and height):
                    # default to viewBox
                    width = vb_w
                    height = vb_h
                if not vb:
                    # default to width, height, and no offset
                    vb_x = 0.0
                    vb_y = 0.0
                    vb_w = width
                    vb_h = height

                self.px2mm = width/vb_w

                if unit == 'mm':
                    # great, the svg file already uses mm
                    pass
                    log.info("px2mm by svg mm unit")
                elif unit == 'in':
                    # prime for inch to mm conversion
                    self.px2mm *= 25.4
                    log.info("px2mm by svg inch unit")
                elif unit == 'cm':
                    # prime for cm to mm conversion
                    self.px2mm *= 10.0
                    log.info("px2mm by svg cm unit")
                elif unit == 'px' or unit == '':
                    # no physical units in file
                    # we have to interpret user (px) units
                    # 3. For some apps we can make a good guess.
                    svghead = svgstring[0:400]
                    if 'Inkscape' in svghead:
                        self.px2mm *= 25.4/90.0
                        log.info("SVG exported with Inkscape -> 90dpi.")
                    elif 'Illustrator' in svghead:
                        self.px2mm *= 25.4/72.0
                        log.info("SVG exported with Illustrator -> 72dpi.")
                    elif 'Intaglio' in svghead:
                        self.px2mm *= 25.4/72.0
                        log.info("SVG exported with Intaglio -> 72dpi.")
                    elif 'CorelDraw' in svghead:
                        self.px2mm *= 25.4/96.0
                        log.info("SVG exported with CorelDraw -> 96dpi.")
                    elif 'Qt' in svghead:
                        self.px2mm *= 25.4/90.0
                        log.info("SVG exported with Qt lib -> 90dpi.")
                    else:
                        # give up in this step
                        self.px2mm = None
                else:
                    log.error("SVG with unsupported unit.")
                    self.px2mm = None

        # 4. Get px2mm by the ratio of svg size to target size
        if not self.px2mm and (width and height):
            self.px2mm = self._target_size[0]/width
            log.info("px2mm by target_size/page_size ratio")


        # 5. Fall back on px unit DPIs default value
        if not self.px2mm:
            log.warn("Failed to determin physical dimensions -> defaulting to 90dpi.")
            self.px2mm = 25.4/90.0

        # adjust tolerances to px units
        self.tolerance2_px = (self.tolerance/self.px2mm)*(self.tolerance/self.px2mm)

        # translation from viewbox
        if vb_x:
            tx = vb_x
        else:
            tx = 0.0
        if vb_y:
            ty = vb_y
        else:
            ty = 0.0

        # let the fun begin
        # recursively parse children
        # output will be in self.boundarys
        node = {
            'xformToWorld': [1,0,0,1,tx,ty],
            'display': 'visible',
            'visibility': 'visible',
            'fill': '#000000',
            'stroke': '#000000',
            'color': '#000000',
            'fill-opacity': 1.0,
            'stroke-opacity': 1.0,
            'opacity': 1.0
        }

        self._tagReader.px2mm = self.px2mm
        self.parse_children(svgRootElement, node)

        # build result dictionary
        parse_results = {'boundarys':self.boundarys, 'dpi':round(25.4/self.px2mm)}
        if self.lasertags:
            parse_results['lasertags'] = self.lasertags

        return parse_results

    def parse_children(self, domNode, parentNode):
        for child in domNode:
            # log.debug("considering tag: " + child.tag)
            if self._tagReader.has_handler(child):
                # 1. setup a new node
                # and inherit from parent
                node = {
                    'paths': [],
                    'xform': [1, 0, 0, 1, 0, 0],
                    'xformToWorld': parentNode['xformToWorld'],
                    'display': parentNode.get('display'),
                    'visibility': parentNode.get('visibility'),
                    'fill': parentNode.get('fill'),
                    'stroke': parentNode.get('stroke'),
                    'color': parentNode.get('color'),
                    'fill-opacity': parentNode.get('fill-opacity'),
                    'stroke-opacity': parentNode.get('stroke-opacity'),
                    'opacity': parentNode.get('opacity')
                }

                # 2. parse child
                # with current attributes and transformation
                self._tagReader.read_tag(child, node)

                # 3. compile boundarys + conversions
                for path in node['paths']:
                    if path:  # skip if empty subpath
                        # 3a.) convert to world coordinates and then to mm units
                        for vert in path:
                            # print isinstance(vert[0],float) and isinstance(vert[1],float)
                            matrixApply(node['xformToWorld'], vert)
                            vertexScale(vert, self.px2mm)
                        # 3b.) sort output by color
                        hexcolor = node['stroke']
                        if hexcolor in self.boundarys:
                            self.boundarys[hexcolor].append(path)
                        else:
                            self.boundarys[hexcolor] = [path]

                # 4. any lasertags (cut settings)?
                if 'lasertags' in node:
                    self.lasertags.extend(node['lasertags'])

                # recursive call
                self.parse_children(child, node)
Exemplo n.º 4
0
class SVGReader:
    """SVG parser.

    Usage:
    reader = SVGReader(0.08, [1220,610])
    boundarys = reader.parse(open('filename').read())
    """
    def __init__(self, tolerance, target_size):
        # parsed path data, paths by color
        # {'#ff0000': [[[x,y], [x,y], ...], [], ..], '#0000ff':[]}
        # Each path is a list of vertices which is a list of two floats.
        self.boundarys = {}

        # the conversion factor to physical dimensions
        # applied to all coordinates in the SVG
        self.px2mm = None

        # what the svg size (typically page dimensions) should be mapped to
        self._target_size = target_size

        # tolerance settings, used in tessalation, path simplification, etc
        self.tolerance = tolerance
        self.tolerance2 = tolerance**2
        self.tolerance2_half = (0.5 * tolerance)**2
        self.tolerance2_px = None

        # init helper object for tag reading
        self._tagReader = SVGTagReader(self)

        # lasersaur cut setting from SVG file
        # list of triplets ... [(pass#, key, value), ...]
        # pass# designates the pass this lasertag controls
        # key is the kind of setting (one of: intensity, feedrate, color)
        # value is the actual value to use
        self.lasertags = []

        # # tags that should not be further traversed
        # self.ignore_tags = {'defs':None, 'pattern':None, 'clipPath':None}

    def parse(self, svgstring, force_dpi=None):
        """ Parse a SVG document.

        This traverses through the document tree and collects all path
        data and converts it to polylines of the requested tolerance.

        Path data is returned as paths by color:
        {'#ff0000': [[path0, path1, ..], [path0, ..], ..]}
        Each path is a list of vertices which is a list of two floats.

        Determining Physical Dimensions
        -------------------------------
        SVG files may use physical units (mm, in) or screen units (px).
        For obvious reason former are preferred as these take out any
        guess-work of how to interpret any coordinates.

        A good SVG authoring app writes physical dimensions to file like this:
        - the svg tag has a width, height, viewBox attribute
        - width and height contains the page dimensions and unit
        - viewBox defines a rectangle with (x, y, width, height)
        - width/viewBox:width is the factor that needs to be applied to
          any (unit-less) coordinates in the file
        - x,y is a translation that needs to be applied to any coordinates

        One issue with svg documents is that they are not always clear on
        the physical dimensions. Often they lack or use px units in the
        width/height attributes (no units implies px units in the SVG
        standard). For example, it's possible to encounter px
        units in the file even when the authoring app interprets these
        as physical units (e.g mm). This means there is an implied DPI
        conversion in the app that we need to guess/know.

        The following strategy is used to get physical dimensions:

        1. from argument (force_dpi)
        2. from units of svg width/height and viewBox
        3. from hints of (known) originating apps
        4. from ratio of page and target size
        5. defaults to 90 DPI
        """
        self.px2mm = None
        self.boundarys = {}

        vb_x = None
        vb_y = None
        vb_w = None
        vb_h = None

        # parse xml
        svgRootElement = ET.fromstring(svgstring)
        tagName = self._tagReader._get_tag(svgRootElement)

        if tagName != 'svg':
            log.error("Invalid file, no 'svg' tag found.")
            return self.boundarys

        # 1. Get px2mm from argument
        if force_dpi is not None:
            self.px2mm = 25.4 / force_dpi
            log.info("SVG import forced to %s dpi." % (force_dpi))

        # Get width, height, viewBox for further processing
        if not self.px2mm:
            width = None
            height = None
            unit = ''

            # get width, height, unit
            width_str = svgRootElement.attrib.get('width')
            height_str = svgRootElement.attrib.get('height')
            if width_str and height_str:
                width, width_unit = parseScalar(width_str)
                height, height_unit = parseScalar(height_str)
                if width_unit != height_unit:
                    log.error("Conflicting units found.")
                unit = width_unit
                log.info("SVG w,h (unit) is %s,%s (%s)." %
                         (width, height, unit))

            # get viewBox
            # http://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute
            vb = svgRootElement.attrib.get('viewBox')
            if vb:
                vb_x, vb_y, vb_w, vb_h = parseFloats(vb)
                log.info("SVG viewBox (%s,%s,%s,%s)." %
                         (vb_x, vb_y, vb_w, vb_h))

        # 2. Get px2mm from width, height, viewBox
        if not self.px2mm:
            if (width and height) or vb:
                if not (width and height):
                    # default to viewBox
                    width = vb_w
                    height = vb_h
                if not vb:
                    # default to width, height, and no offset
                    vb_x = 0.0
                    vb_y = 0.0
                    vb_w = width
                    vb_h = height

                self.px2mm = width / vb_w

                if unit == 'mm':
                    # great, the svg file already uses mm
                    pass
                    log.info("px2mm by svg mm unit")
                elif unit == 'in':
                    # prime for inch to mm conversion
                    self.px2mm *= 25.4
                    log.info("px2mm by svg inch unit")
                elif unit == 'cm':
                    # prime for cm to mm conversion
                    self.px2mm *= 10.0
                    log.info("px2mm by svg cm unit")
                elif unit == 'px' or unit == '':
                    # no physical units in file
                    # we have to interpret user (px) units
                    # 3. For some apps we can make a good guess.
                    svghead = svgstring[0:400]
                    if 'Inkscape' in svghead:
                        self.px2mm *= 25.4 / 90.0
                        log.info("SVG exported with Inkscape -> 90dpi.")
                    elif 'Illustrator' in svghead:
                        self.px2mm *= 25.4 / 72.0
                        log.info("SVG exported with Illustrator -> 72dpi.")
                    elif 'Intaglio' in svghead:
                        self.px2mm *= 25.4 / 72.0
                        log.info("SVG exported with Intaglio -> 72dpi.")
                    elif 'CorelDraw' in svghead:
                        self.px2mm *= 25.4 / 96.0
                        log.info("SVG exported with CorelDraw -> 96dpi.")
                    elif 'Qt' in svghead:
                        self.px2mm *= 25.4 / 90.0
                        log.info("SVG exported with Qt lib -> 90dpi.")
                    else:
                        # give up in this step
                        self.px2mm = None
                else:
                    log.error("SVG with unsupported unit.")
                    self.px2mm = None

        # 4. Get px2mm by the ratio of svg size to target size
        if not self.px2mm and (width and height):
            self.px2mm = self._target_size[0] / width
            log.info("px2mm by target_size/page_size ratio")

        # 5. Fall back on px unit DPIs default value
        if not self.px2mm:
            log.warn(
                "Failed to determin physical dimensions -> defaulting to 90dpi."
            )
            self.px2mm = 25.4 / 90.0

        # adjust tolerances to px units
        self.tolerance2_px = (self.tolerance / self.px2mm) * (self.tolerance /
                                                              self.px2mm)

        # translation from viewbox
        if vb_x:
            tx = vb_x
        else:
            tx = 0.0
        if vb_y:
            ty = vb_y
        else:
            ty = 0.0

        # let the fun begin
        # recursively parse children
        # output will be in self.boundarys
        node = {
            'xformToWorld': [1, 0, 0, 1, tx, ty],
            'display': 'visible',
            'visibility': 'visible',
            'fill': '#000000',
            'stroke': '#000000',
            'color': '#000000',
            'fill-opacity': 1.0,
            'stroke-opacity': 1.0,
            'opacity': 1.0
        }

        self._tagReader.px2mm = self.px2mm
        self.parse_children(svgRootElement, node)

        # build result dictionary
        parse_results = {
            'boundarys': self.boundarys,
            'dpi': round(25.4 / self.px2mm)
        }
        if self.lasertags:
            parse_results['lasertags'] = self.lasertags

        return parse_results

    def parse_children(self, domNode, parentNode):
        for child in domNode:
            # log.debug("considering tag: " + child.tag)
            if self._tagReader.has_handler(child):
                # 1. setup a new node
                # and inherit from parent
                node = {
                    'paths': [],
                    'xform': [1, 0, 0, 1, 0, 0],
                    'xformToWorld': parentNode['xformToWorld'],
                    'display': parentNode.get('display'),
                    'visibility': parentNode.get('visibility'),
                    'fill': parentNode.get('fill'),
                    'stroke': parentNode.get('stroke'),
                    'color': parentNode.get('color'),
                    'fill-opacity': parentNode.get('fill-opacity'),
                    'stroke-opacity': parentNode.get('stroke-opacity'),
                    'opacity': parentNode.get('opacity')
                }

                # 2. parse child
                # with current attributes and transformation
                self._tagReader.read_tag(child, node)

                # 3. compile boundarys + conversions
                for path in node['paths']:
                    if path:  # skip if empty subpath
                        # 3a.) convert to world coordinates and then to mm units
                        for vert in path:
                            # print isinstance(vert[0],float) and isinstance(vert[1],float)
                            matrixApply(node['xformToWorld'], vert)
                            vertexScale(vert, self.px2mm)
                        # 3b.) sort output by color
                        hexcolor = node['stroke']
                        if hexcolor in self.boundarys:
                            self.boundarys[hexcolor].append(path)
                        else:
                            self.boundarys[hexcolor] = [path]

                # 4. any lasertags (cut settings)?
                if 'lasertags' in node:
                    self.lasertags.extend(node['lasertags'])

                # recursive call
                self.parse_children(child, node)