def pointsAttrib(self, node, attr, value): """Read the 'points' attribute.""" floats = parseFloats(value) if len(floats) % 2 == 0: node[attr] = floats else: log.error("odd number of vertices")
def _parseColor(self, val): """ Parse a color definition. Returns a color in hex format, 'inherit', or 'none'. 'none' means that the geometry is not to be rendered. See: http://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint """ # http://www.w3.org/TR/SVG11/color.html # http://www.w3.org/TR/2008/REC-CSS2-20080411/syndata.html#color-units if val[0] == " ": val = val.strip() if val[0] == '#': return normalize_hex(val) elif val.startswith('rgba'): floats = parseFloats(val[5:-1]) if len(floats) == 4: log.warn("opacity in rgba is ignored, \ use stroke-opacity/fill-opacity instead") return rgb_to_hex(tuple(floats[:3])) elif val.startswith('rgb'): floats = parseFloats(val[4:-1]) if len(floats) == 3: return rgb_to_hex(tuple(floats)) elif val == 'none': # 'none' means the geometry is not to be filled or stroked # http://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint return 'none' elif val.startswith('hsl'): log.warn("hsl/hsla color spaces are not supported") elif val.startswith('url'): log.warn("defs are not supported") elif val in css3_names_to_hex: # named colors return css3_names_to_hex[val] elif val in ['currentColor', 'inherit']: return 'inherit' else: log.warn("invalid color, skipped: " + str(val)) return 'inherit'
def _parseColor(self, val): """ Parse a color definition. Returns a color in hex format, 'inherit', or 'none'. 'none' means that the geometry is not to be rendered. See: http://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint """ # http://www.w3.org/TR/SVG11/color.html # http://www.w3.org/TR/2008/REC-CSS2-20080411/syndata.html#color-units if val[0] == " ": val = val.strip() if val[0] == '#': return normalize_hex(val) elif val.startswith('rgba'): floats = parseFloats(val[5:-1]) if len(floats) == 4: log.warn("opacity in rgba is ignored, \ use stroke-opacity/fill-opacity instead") return rgb_to_hex(tuple(floats[:3])) elif val.startswith('rgb'): floats = parseFloats(val[4:-1]) if len(floats) == 3: return rgb_to_hex(tuple(floats)) elif val == 'none': # 'none' means the geometry is not to be filled or stroked # http://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint return 'none' elif val.startswith('hsl'): log.warn("hsl/hsla color spaces are not supported") elif val.startswith('url'): log.warn("defs are not supported"); elif val in css3_names_to_hex: # named colors return css3_names_to_hex[val] elif val in ['currentColor', 'inherit']: return 'inherit' else: log.warn("invalid color, skipped: " + str(val)) return 'inherit'
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(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 transformAttrib(self, node, attr, value): # http://www.w3.org/TR/SVG11/coords.html#EstablishingANewUserSpace xforms = [] matches = self.re_findall_transforms(value) # this parses something like "translate(50,50), rotate(56)"" to # [('translate(50,50)', 'translate', '50,50'), ('rotate(56)', 'rotate', '56')] for match in matches: xformKind = match[1] params = parseFloats(match[2]) # translate if xformKind == 'translate': if len(params) == 1: xforms.append([1, 0, 0, 1, params[0], params[0]]) elif len(params) == 2: xforms.append([1, 0, 0, 1, params[0], params[1]]) else: log.warn('translate skipped; invalid num of params') # rotate elif xformKind == 'rotate': if len(params) == 3: angle = params[0] * self.DEG_TO_RAD xforms.append([1, 0, 0, 1, params[1], params[2]]) xforms.append([ math.cos(angle), math.sin(angle), -math.sin(angle), math.cos(angle), 0, 0 ]) xforms.append([1, 0, 0, 1, -params[1], -params[2]]) elif len(params) == 1: angle = params[0] * self.DEG_TO_RAD xforms.append([ math.cos(angle), math.sin(angle), -math.sin(angle), math.cos(angle), 0, 0 ]) else: log.warn('rotate skipped; invalid num of params') #scale elif xformKind == 'scale': if len(params) == 1: xforms.append([params[0], 0, 0, params[0], 0, 0]) elif len(params) == 2: xforms.append([params[0], 0, 0, params[1], 0, 0]) else: log.warn('scale skipped; invalid num of params') # matrix elif xformKind == 'matrix': if len(params) == 6: xforms.append(params) else: log.warn('matrix skipped; invalid num of params') # skewX elif xformKind == 'skewX': if len(params) == 1: angle = params[0] * self.DEG_TO_RAD xforms.append([1, 0, math.tan(angle), 1, 0, 0]) else: log.warn('skewX skipped; invalid num of params') # skewY elif xformKind == 'skewY': if len(params) == 1: angle = params[0] * self.DEG_TO_RAD xforms.append([1, math.tan(angle), 0, 1, 0, 0]) else: log.warn('skewY skipped; invalid num of params') #calculate combined transformation matrix xform_combined = [1, 0, 0, 1, 0, 0] for xform in xforms: xform_combined = matrixMult(xform_combined, xform) # assign node['xform'] = xform_combined
def transformAttrib(self, node, attr, value): # http://www.w3.org/TR/SVG11/coords.html#EstablishingANewUserSpace xforms = [] matches = self.re_findall_transforms(value) # this parses something like "translate(50,50), rotate(56)"" to # [('translate(50,50)', 'translate', '50,50'), ('rotate(56)', 'rotate', '56')] for match in matches: xformKind = match[1] params = parseFloats(match[2]) # translate if xformKind == 'translate': if len(params) == 1: xforms.append([1, 0, 0, 1, params[0], params[0]]) elif len(params) == 2: xforms.append([1, 0, 0, 1, params[0], params[1]]) else: log.warn('translate skipped; invalid num of params') # rotate elif xformKind == 'rotate': if len(params) == 3: angle = params[0] * self.DEG_TO_RAD xforms.append([1, 0, 0, 1, params[1], params[2]]) xforms.append([math.cos(angle), math.sin(angle), -math.sin(angle), math.cos(angle), 0, 0]) xforms.append([1, 0, 0, 1, -params[1], -params[2]]) elif len(params) == 1: angle = params[0] * self.DEG_TO_RAD xforms.append([math.cos(angle), math.sin(angle), -math.sin(angle), math.cos(angle), 0, 0]) else: log.warn('rotate skipped; invalid num of params') #scale elif xformKind == 'scale': if len(params) == 1: xforms.append([params[0], 0, 0, params[0], 0, 0]) elif len(params) == 2: xforms.append([params[0], 0, 0, params[1], 0, 0]) else: log.warn('scale skipped; invalid num of params') # matrix elif xformKind == 'matrix': if len(params) == 6: xforms.append(params) else: log.warn('matrix skipped; invalid num of params') # skewX elif xformKind == 'skewX': if len(params) == 1: angle = params[0]*self.DEG_TO_RAD xforms.append([1, 0, math.tan(angle), 1, 0, 0]) else: log.warn('skewX skipped; invalid num of params') # skewY elif xformKind == 'skewY': if len(params) == 1: angle = params[0]*self.DEG_TO_RAD xforms.append([1, math.tan(angle), 0, 1, 0, 0]) else: log.warn('skewY skipped; invalid num of params') #calculate combined transformation matrix xform_combined = [1,0,0,1,0,0] for xform in xforms: xform_combined = matrixMult(xform_combined, xform) # assign node['xform'] = xform_combined