Source

PCBmodE / utils / extract.py

#!/usr/bin/python

#   Copyright 2013 Boldport Limited
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import os
import re
import json
import hashlib
from lxml import etree as et


# pcbmode modules
import svg
import utils
import inkscape
from Point import Point


def extract_routing(cfg):
    """
    Extracts routing paths and vias from an input SVG file, and saves the data
    as a dictionary, and then dumps it into a JSON file.
    """

    r = []

    input_file = os.path.join(cfg['base_dir'], cfg['pcbmode']['locations']['build'],
                              cfg['board']['files'].get('routing_svg') or cfg['board_name'] + '.svg')
    output_file = os.path.join(cfg['base_dir'],
                              cfg['board']['files'].get('routing_json') or cfg['board_name'] + '_routing.json')

    try:
        routing_file = open(os.path.join(input_file), 'rb')
    except IOError as e:
        print "ERROR: can't open %s; has the board been created yet?" % input_file
        print "I/O error({0}): {1}".format(e.errno, e.strerror)
        raise

    svg_in = et.ElementTree(file=routing_file) 
    routing_file.close()

    routing_info = {'routes':{}, 'vias':{}}
    
    xpath_expr = "//svg:g[@inkscape:label='%s']//svg:g[@inkscape:label='routing']//svg:path[@d]"

    path_effect_ids = []
    generated_paths = {}

    # extract routes
    routing = routing_info['routes']
    for layer in utils.get_surface_layers(cfg):
        routing[layer] = {}
        for i, path in enumerate(svg_in.xpath(xpath_expr % layer, namespaces={'inkscape':cfg['namespace']['inkscape'], 'svg':cfg['namespace']['svg']})):
            routing[layer][str(i)] = {}
            rd = svg.absolute_to_relative_path(path.get('d'))
            gerber_lp = path.get('gerber_lp')
            path_style = path.get('style')
            path_type = path.get('type')
            pcbmode_params = path.get('pcbmode') 

            # get style information (mostyle to distinguish from 'fill' and 'stroke'
            # routing
            if path_style is not None:
                regex = r".*?%s:\s?(?P<s>[^;]*)(?:;|$)"
                stroke_def = re.match(regex % 'stroke', path_style)

                if stroke_def is not None:
                    stroke = stroke_def.group('s')

                stroke_width_def = re.match(regex % 'stroke-width', path_style)    
                if stroke_width_def is not None:
                    stroke_width = float(stroke_width_def.group('s'))
                else:
                    stroke_width = None

                fill = re.match(regex % 'fill', path_style)
                if fill is not None:
                    fill = fill.group('s')

                path_style = ''

                #path_style = "stroke:%s;stroke-width:%s;" % (stroke, stroke_width)
                if stroke_width is not None:
                    path_style += '%s:%s;' % ('stroke-width', stroke_width)
                if fill in ['none']:
                    path_style += '%s:%s;' % ('fill', 'none')


            # add attributes to dict
            routing[layer][str(i)]['d'] = str(rd)
            if gerber_lp is not None:
                routing[layer][str(i)]['gerber_lp'] = str(gerber_lp)
            if path_style is not None:
                routing[layer][str(i)]['style'] = path_style

            if pcbmode_params is not None:
                pt = re.match('^\s*([^:]*)', pcbmode_params)
                if pt.group(0).lower() == 'meander':
                    # creates a meander SVG path
                    meander_params = utils.process_meander_type(pcbmode_params)
                    generated_path = svg.create_meandering_path(meander_params)
                routing[layer][str(i)]['pcbmode'] = pcbmode_params


            if path_type is not None:
                routing[layer][str(i)]['type'] = path_type


            # if this is a path with a pattern, keep the required inkscape 
            # namespace parameters
            for param in ['original-d', 'connector-curvature', 'path-effect']:
                inkparam = path.get('{'+cfg['namespace']['inkscape']+'}%s' % param)
                if inkparam is not None:
                    routing[layer][str(i)]['inkscape:'+param] = inkparam
                    # special case 'path-effect' in order to get the "effect" that's repeated
                    # along the path
                    if param == 'path-effect':
                        path_effect_ids.append(inkparam.lstrip('#'))
                        if generated_path is not None:
                            generated_paths[inkparam.lstrip('#')] = generated_path


            generated_path = None

    # add path-effects to routing file
    path_effects = svg_in.xpath("//inkscape:path-effect", namespaces={'inkscape':cfg['namespace']['inkscape']})
    if len(path_effects) > 0:
        routing['path_effects'] = {}
    for path_effect in path_effects:
        peid = path_effect.get('id')
        if peid in path_effect_ids:
            routing['path_effects'][peid] = {}
            for name, value in path_effect.items():
                routing['path_effects'][peid][name] = value
            if peid in generated_paths:
                routing['path_effects'][peid]['pattern'] = generated_paths[peid]
         



    regex = r".*?translate\s?\(\s?(?P<x>-?[0-9]*\.?[0-9]+)\s?[\s,]\s?(?P<y>-?[0-9]*\.?[0-9]+\s?)\s?\).*"

    vias = routing_info['vias']

    i = 0

    # extract NEW vias (only in 'top' surface layer)
    xpath_expr = "//svg:g[@inkscape:label='%s']//svg:g[@inkscape:label='%s']"
    top_copper_layer = svg_in.find(xpath_expr % ('top', 'copper'), namespaces={'inkscape':cfg['namespace']['inkscape'],'svg':cfg['namespace']['svg']})
    paths = top_copper_layer.xpath('//svg:path[starts-with(@inkscape:label, "%s")]' % cfg['via_prefix'], namespaces={'inkscape':cfg['namespace']['inkscape'],'svg':cfg['namespace']['svg']})
    for i, path in enumerate(paths):
        # get via type
        via_type = path.get('{'+cfg['namespace']['inkscape']+'}label')
        # check if via is specified, or 'default' is specified:
        via_part_name = via_type[len(cfg['via_prefix']):]
        via_part_name = via_part_name.strip(' ') # remove leading spaces if there any
        possible_answers = ['', 'default', 'default_via', 'default via']
        if via_part_name.lower() in possible_answers:
            via_type = cfg['via_prefix'] + cfg['board']['vias'].get('default_via') or 'VIA'
        # get location of via
        location = Point(path.get('{'+cfg['namespace']['sodipodi']+'}cx'), 
                         path.get('{'+cfg['namespace']['sodipodi']+'}cy'))
        location.y = -location.y
        transform = path.get('transform')
        if transform is not None: 
            tr = re.match(regex, transform)
            location.x += float(tr.group('x'))
            location.y -= float(tr.group('y')) 
        # get radius of via
        radius_x = path.get('{'+cfg['namespace']['sodipodi']+'}rx')
        radius_y = path.get('{'+cfg['namespace']['sodipodi']+'}ry')
        vias[str(i)] = {"via_type": via_type, 
                        "location": [str(location.x), str(location.y)], 
                        "radius_x": radius_x, 
                        "radius_y": radius_y}

    tmp = Point()
    transform = Point()

    # extract existing vias; this looks only for the 'drill', so that's the only
    # thing that needs to be deleted in order to delete the entire via
    xpath_expr = "//svg:g[@inkscape:label='%s']//svg:g[starts-with(@refdef,'%s')]//svg:path"
    paths = svg_in.xpath(xpath_expr % ('drills', 'via:'), namespaces={'inkscape':cfg['namespace']['inkscape'],'svg':cfg['namespace']['svg']})
    for i, path in enumerate(paths, i+1):

        # get type
        tmp = path.getparent()
        via_type = tmp.get('refdef')

        tr = re.match(regex, path.get('transform') or '')
        if tr:
            transform.assign(tr.group('x'), tr.group('y'))
        else:
            transform.assign(0, 0)

        # TODO: this is a bit of a brittle hack that will break when the structure
        # changes.
        ancestors = path.xpath("ancestor::*[@transform]")

        t = ancestors[len(ancestors)-1].get('transform')
        tr = re.match(regex, t)
        if tr:
            transform.assign(tr.group('x'), tr.group('y'))

        radius = float(path.get('diameter')) / 2
        vias[str(i)] = {"via_type": via_type,
                        "location": [str(transform.x), str(-transform.y)], 
                        "radius_x": radius, 
                        "radius_y": radius}

    try:
        f = open(output_file, 'wb')
    except IOError as e:
        print "I/O error({0}): {1}".format(e.errno, e.strerror)

    f.write(json.dumps(routing_info, sort_keys=True, indent=2))
    f.close()   

    return