summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerbonara/gerber/cam.py4
-rwxr-xr-xgerbonara/gerber/excellon.py99
-rw-r--r--gerbonara/gerber/layer_rules.py26
-rw-r--r--gerbonara/gerber/layers.py136
-rw-r--r--gerbonara/gerber/rs274x.py10
-rw-r--r--gerbonara/gerber/tests/test_layers.py76
-rw-r--r--gerbonara/gerber/utils.py1
7 files changed, 235 insertions, 117 deletions
diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py
index ccfa7a2..411bc65 100644
--- a/gerbonara/gerber/cam.py
+++ b/gerbonara/gerber/cam.py
@@ -152,8 +152,8 @@ class FileSettings:
class CamFile:
- def __init__(self, filename=None, layer_name=None):
- self.filename = filename
+ def __init__(self, original_path=None, layer_name=None):
+ self.original_path = original_path
self.layer_name = layer_name
self.import_settings = None
self.objects = []
diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py
index 3931660..b3fbd97 100755
--- a/gerbonara/gerber/excellon.py
+++ b/gerbonara/gerber/excellon.py
@@ -71,15 +71,22 @@ def parse_allegro_ncparam(data, settings=None):
# still be able to extract the same information from the human-readable ncdrill.log.
if settings is None:
- self.settings = FileSettings(number_format=(None, None))
+ settings = FileSettings(number_format=(None, None))
lz_supp, tz_supp = False, False
+ nf_int, nf_frac = settings.number_format
for line in data.splitlines():
line = re.sub(r'\s+', ' ', line.strip())
if (match := re.fullmatch(r'FORMAT ([0-9]+\.[0-9]+)', line)):
x, _, y = match[1].partition('.')
- settings.number_format = int(x), int(y)
+ nf_int, nf_frac = int(x), int(y)
+
+ elif (match := re.fullmatch(r'INTEGER-PLACES ([0-9]+)', line)):
+ nf_int = int(match[1])
+
+ elif (match := re.fullmatch(r'DECIMAL-PLACES ([0-9]+)', line)):
+ nf_frac = int(match[1])
elif (match := re.fullmatch(r'COORDINATES (ABSOLUTE|.*)', line)):
# I have not been able to find a single incremental-notation allegro file. Probably that is for the better.
@@ -100,17 +107,59 @@ def parse_allegro_ncparam(data, settings=None):
raise SyntaxError('Allegro Excellon parameters specify both leading and trailing zero suppression. We do not '
'know how to parse this. Please raise an issue on our issue tracker and provide an example file.')
+ settings.number_format = nf_int, nf_frac
settings.zeros = 'leading' if lz_supp else 'trailing'
+ return settings
+
+
+def parse_allegro_logfile(data):
+ found_tools = {}
+ unit = None
+
+ for line in data.splitlines():
+ line = line.strip()
+ line = re.sub('\s+', ' ', line)
+
+ if (m := re.match(r'OUTPUT-UNITS (METRIC|ENGLISH|INCHES)', line)):
+ # I have no idea wth is the difference between "ENGLISH" and "INCHES". I think one might just be the one
+ # Allegro uses in footprint files, with the other one being used in gerber exports.
+ unit = MM if m[1] == 'METRIC' else Inch
+
+ elif (m := re.match(r'T(?P<index1>[0-9]+) (?P<index2>[0-9]+)\. (?P<diameter>[0-9/.]+) [0-9. /+-]* (?P<plated>PLATED|NON_PLATED|OPTIONAL) [0-9]+', line)):
+ index1, index2 = int(m['index1']), int(m['index2'])
+ if index1 != index2:
+ return {}
+ diameter = float(m['diameter'])
+ if unit == Inch:
+ diameter /= 1000
+ is_plated = None if m['plated'] is None else (m['plated'] in ('PLATED', 'OPTIONAL'))
+ found_tools[index1] = ExcellonTool(diameter=diameter, plated=is_plated, unit=unit)
+ return found_tools
class ExcellonFile(CamFile):
- def __init__(self, objects=None, comments=None, import_settings=None, filename=None, generator_hints=None):
- super().__init__(filename=filename)
+ def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None):
+ super().__init__(original_path=original_path)
self.objects = objects or []
self.comments = comments or []
self.import_settings = import_settings
self.generator_hints = generator_hints or [] # This is a purely informational goodie from the parser. Use it as you wish.
+ def __str__(self):
+ name = f'{self.original_path.name} ' if self.original_path else ''
+ if self.is_plated:
+ plating = 'plated'
+ elif self.is_nonplated:
+ plating = 'nonplated'
+ elif self.is_mixed_plating:
+ plating = 'mixed plating'
+ else:
+ plating = 'unknown plating'
+ return f'<ExcellonFile {name}{plating} with {len(list(self.drills()))} drills, {len(list(self.slots()))} slots using {len(self.drill_sizes())} tools>'
+
+ def __repr__(self):
+ return str(self)
+
def __bool__(self):
return not self.is_empty
@@ -165,6 +214,7 @@ class ExcellonFile(CamFile):
@classmethod
def open(kls, filename, plated=None, settings=None):
filename = Path(filename)
+ logfile_tools = None
# Parse allegro parameter files.
# Prefer nc_param.txt over ncparam.log since the txt is the machine-readable one.
@@ -172,16 +222,23 @@ class ExcellonFile(CamFile):
for fn in 'nc_param.txt', 'ncdrill.log':
if (param_file := filename.parent / fn).is_file():
settings = parse_allegro_ncparam(param_file.read_text())
+ warnings.warn(f'Loaded allegro-style excellon settings file {param_file}')
break
- return kls.from_string(filename.read_text(), settings=settings, filename=filename, plated=plated)
+ # TODO add try/except aronud this
+ log_file = filename.parent / 'ncdrill.log'
+ if log_file.is_file():
+ logfile_tools = parse_allegro_logfile(log_file.read_text())
+
+ return kls.from_string(filename.read_text(), settings=settings,
+ filename=filename, plated=plated, logfile_tools=logfile_tools)
@classmethod
- def from_string(kls, data, settings=None, filename=None, plated=None):
- parser = ExcellonParser(settings)
+ def from_string(kls, data, settings=None, filename=None, plated=None, logfile_tools=None):
+ parser = ExcellonParser(settings, logfile_tools=logfile_tools)
parser.do_parse(data, filename=filename)
return kls(objects=parser.objects, comments=parser.comments, import_settings=settings,
- generator_hints=parser.generator_hints, filename=filename)
+ generator_hints=parser.generator_hints, original_path=filename)
def _generate_statements(self, settings, drop_comments=True):
@@ -309,6 +366,12 @@ class ExcellonFile(CamFile):
def drill_sizes(self):
return sorted({ obj.tool.diameter for obj in self.objects })
+ def drills(self):
+ return (obj for obj in self.objects if isinstance(obj, Flash))
+
+ def slots(self):
+ return (obj for obj in self.objects if not isinstance(obj, Flash))
+
@property
def bounds(self):
if not self.objects:
@@ -330,7 +393,7 @@ class ProgramState(Enum):
class ExcellonParser(object):
- def __init__(self, settings=None):
+ def __init__(self, settings=None, logfile_tools=None):
# NOTE XNC files do not contain an explicit number format specification, but all values have decimal points.
# Thus, we set the default number format to (None, None). If the file does not contain an explicit specification
# and FileSettings.parse_gerber_value encounters a number without an explicit decimal point, it will throw a
@@ -352,6 +415,7 @@ class ExcellonParser(object):
self.generator_hints = []
self.lineno = None
self.filename = None
+ self.logfile_tools = logfile_tools or {}
def warn(self, msg):
warnings.warn(f'{self.filename}:{self.lineno} "{self.line}": {msg}', SyntaxWarning)
@@ -462,9 +526,17 @@ class ExcellonParser(object):
if index == 0: # T0 is used as END marker, just ignore
return
elif index not in self.tools:
- raise SyntaxError(f'Undefined tool index {index} selected.')
+ if not self.tools and index in self.logfile_tools:
+ # allegro is just wonderful.
+ self.warn(f'Undefined tool index {index} selected. We found an allegro drill log file next to this, so '
+ 'we will use tool definitions from there.')
+ self.active_tool = self.logfile_tools[index]
+
+ else:
+ raise SyntaxError(f'Undefined tool index {index} selected.')
- self.active_tool = self.tools[index]
+ else:
+ self.active_tool = self.tools[index]
coord = lambda name, key=None: fr'({name}(?P<{key or name}>[+-]?[0-9]*\.?[0-9]*))?'
xy_coord = coord('X') + coord('Y')
@@ -478,8 +550,7 @@ class ExcellonParser(object):
dy = int(match['Y'] or '0')
for i in range(int(match['count'])):
- self.pos[0] += dx
- self.pos[1] += dy
+ self.pos = (self.pos[0] + dx, self.pos[1] + dy)
# FIXME fix API below
if not self.ensure_active_tool():
return
@@ -689,7 +760,7 @@ class ExcellonParser(object):
if match[2] not in ('', '2'):
raise SyntaxError(f'Unsupported FMAT format version {match["version"]}')
- @exprs.match('G40|G41|G42|{coord("F")}')
+ @exprs.match(r'G40|G41|G42|F[0-9]+')
def handle_unhandled(self, match):
self.warn(f'{match[0]} excellon command intended for CAM tools found in EDA file.')
diff --git a/gerbonara/gerber/layer_rules.py b/gerbonara/gerber/layer_rules.py
index 1733964..871e44f 100644
--- a/gerbonara/gerber/layer_rules.py
+++ b/gerbonara/gerber/layer_rules.py
@@ -11,7 +11,7 @@ MATCH_RULES = {
'bottom silk': r'.*\.gbo',
'bottom paste': r'.*\.gbp',
'inner copper': r'.*\.gp?([0-9]+)',
- 'drill outline': r'.*\.(gko|gm[0-9]+)',
+ 'mechanical outline': r'.*\.(gko|gm[0-9]+)',
'drill unknown': r'.*\.(txt)',
},
@@ -25,7 +25,7 @@ MATCH_RULES = {
'bottom silk': r'.*\.gbo|.*b.silks.*',
'bottom paste': r'.*\.gbp|.*b.paste.*',
'inner copper': r'.*\.gp?([0-9]+)|.*inn?e?r?([0-9]+).cu.*',
- 'drill outline': r'.*\.(gm[0-9]+)|.*edge.cuts.*',
+ 'mechanical outline': r'.*\.(gm[0-9]+)|.*edge.cuts.*',
'drill plated': r'.*\.(drl)',
},
@@ -39,7 +39,7 @@ MATCH_RULES = {
'bottom silk': r'.*\.bottomsilk\.\w+',
'bottom paste': r'.*\.bottompaste\.\w+',
'inner copper': r'.*\.inner_l([0-9]+)\.\w+', # FIXME verify this
- 'drill outline': r'.*\.outline\.gbr',
+ 'mechanical outline': r'.*\.outline\.gbr',
'drill plated': r'.*\.plated-drill.cnc',
'drill nonplated': r'.*\.unplated-drill.cnc',
},
@@ -64,10 +64,10 @@ MATCH_RULES = {
'top silk': r'.*\.PosiTop',
'top paste': r'.*\.PasteTop',
'bottom copper': r'.*\.Bot',
- 'bottop mask': r'.*\.StopBot',
- 'bottop silk': r'.*\.PosiBot',
- 'bottop paste': r'.*\.PasteBot',
- 'drill outline': r'.*\.Outline',
+ 'bottom mask': r'.*\.StopBot',
+ 'bottom silk': r'.*\.PosiBot',
+ 'bottom paste': r'.*\.PasteBot',
+ 'mechanical outline': r'.*\.Outline',
'drill plated': r'.*\.Drill',
},
@@ -77,11 +77,11 @@ MATCH_RULES = {
'top silk': r'.*\.sst',
'top paste': r'.*\.spt',
'bottom copper': r'.*\.bot',
- 'bottop mask': r'.*\.smb',
- 'bottop silk': r'.*\.ssb',
- 'bottop paste': r'.*\.spb',
+ 'bottom mask': r'.*\.smb',
+ 'bottom silk': r'.*\.ssb',
+ 'bottom paste': r'.*\.spb',
'inner copper': r'.*\.in([0-9]+)',
- 'drill outline': r'.*\.(fab|drd)',
+ 'mechanical outline': r'.*\.(fab|drd)',
'drill plated': r'.*\.tap',
'drill nonplated': r'.*\.npt',
},
@@ -97,12 +97,12 @@ MATCH_RULES = {
'bottom silk': r'.*(\.pls|\.bsk|\.bottomsilkscreen\.ger)|.*(silkscreen_bottom|bottom_silk).*',
'bottom paste': r'.*(\.crs|\.bsp|\.bcream\.ger)|.*(solderpaste_bottom|bottom_paste).*',
'inner copper': r'.*\.ly([0-9]+)|.*\.internalplane([0-9]+)\.ger',
- 'drill outline': r'.*(\.dim|\.mil|\.gml)|.*\.(?:board)?outline\.ger|profile\.gbr',
+ 'mechanical outline': r'.*(\.dim|\.mil|\.gml)|.*\.(?:board)?outline\.ger|profile\.gbr',
'drill plated': r'.*\.(txt|exc|drd|xln)',
},
'siemens': {
- 'drill outline': r'.*ContourPlated.ncd',
+ 'mechanical outline': r'.*ContourPlated.ncd',
'inner copper': r'.*L([0-9]+).gdo',
'bottom silk': r'.*SilkscreenBottom.gdo',
'top silk': r'.*SilkscreenTop.gdo',
diff --git a/gerbonara/gerber/layers.py b/gerbonara/gerber/layers.py
index a439de3..69f9316 100644
--- a/gerbonara/gerber/layers.py
+++ b/gerbonara/gerber/layers.py
@@ -30,7 +30,7 @@ from .layer_rules import MATCH_RULES
STANDARD_LAYERS = [
- 'outline',
+ 'mechanical outline',
'top copper',
'top mask',
'top silk',
@@ -49,10 +49,12 @@ def match_files(filenames):
matches[generator] = gen
for layer, regex in rules.items():
for fn in filenames:
- if (m := re.fullmatch(regex, fn.name.lower())):
+ if (m := re.fullmatch(regex, fn.name, re.IGNORECASE)):
if layer == 'inner copper':
- layer = 'inner_' + ''.join(m.groups()) + ' copper'
- gen[layer] = gen.get(layer, []) + [fn]
+ target = 'inner_' + ''.join(e or '' for e in m.groups()) + ' copper'
+ else:
+ target = layer
+ gen[target] = gen.get(target, []) + [fn]
return matches
def best_match(filenames):
@@ -88,59 +90,67 @@ def common_prefix(l):
def autoguess(filenames):
prefix = common_prefix([f.name for f in filenames])
- matches = { layername_autoguesser(f.name[len(prefix):] if f.name.startswith(prefix) else f.name): f
- for f in filenames }
+ matches = {}
+ for f in filenames:
+ name = layername_autoguesser(f.name[len(prefix):] if f.name.startswith(prefix) else f.name)
+ if name != 'unknown unknown':
+ matches[name] = matches.get(name, []) + [f]
inner_layers = [ m for m in matches if 'inner' in m ]
- if len(inner_layers) >= 4 and not 'copper top' in matches and not 'copper bottom' in matches:
- matches['copper top'] = matches.pop('copper inner1')
- last_inner = sorted(inner_layers, key=lambda name: int(name.partition(' ')[0].partition('_')[2]))[-1]
- matches['copper bottom'] = matches.pop(last_inner)
+ if len(inner_layers) >= 4 and 'copper top' not in matches and 'copper bottom' not in matches:
+ if 'inner_01 copper' in matches:
+ warnings.warn('Could not find copper layer. Re-assigning outermost inner layers to top/bottom copper.')
+ matches['top copper'] = matches.pop('inner_01 copper')
+ last_inner = sorted(inner_layers, key=lambda name: int(name.partition(' ')[0].partition('_')[2]))[-1]
+ matches['bottom copper'] = matches.pop(last_inner)
return matches
def layername_autoguesser(fn):
- fn, _, _ext = fn.lower().rpartition('.')
+ fn, _, ext = fn.lower().rpartition('.')
+
+ if ext == 'log':
+ return 'unknown unknown'
side, use = 'unknown', 'unknown'
- if re.match('top|front|pri?m?(ary)?', fn):
+ if re.search('top|front|pri?m?(ary)?', fn):
side = 'top'
use = 'copper'
- if re.match('bot(tom)?|back|sec(ondary)?', fn):
+ if re.search('bot(tom)?|back|sec(ondary)?', fn):
side = 'bottom'
use = 'copper'
- if re.match('silks?(creen)?', fn):
+ if re.search('silks?(creen)?', fn):
use = 'silk'
- elif re.match('(solder)?paste', fn):
+ elif re.search('(solder)?paste', fn):
use = 'paste'
- elif re.match('(solder)?mask', fn):
+ elif re.search('(solder)?mask', fn):
use = 'mask'
- elif (m := re.match(r'(la?y?e?r?|in(ner)?)\W*(?P<num>[0-9]+)', fn)):
- use = 'copper'
- side = f'inner_{m["num"]:02d}'
-
- elif re.match('film', fn):
- use = 'copper'
-
- elif re.match('out(line)?', fn):
- use = 'drill'
- side = 'outline'
-
- elif re.match('drill|rout?e?', fn):
+ elif re.search('drill|rout?e?', fn):
use = 'drill'
side = 'unknown'
- if re.match(r'np(th)?|(non|un)\W*plated|(non|un)\Wgalv', fn):
+ if re.search(r'np(th)?|(non|un)\W*plated|(non|un)\Wgalv', fn):
side = 'nonplated'
- elif re.match('pth|plated|galv', fn):
+ elif re.search('pth|plated|galv', fn):
side = 'plated'
- return f'{use} {side}'
+ elif (m := re.search(r'(la?y?e?r?|in(ner)?)\W*(?P<num>[0-9]+)', fn)):
+ use = 'copper'
+ side = f'inner_{int(m["num"]):02d}'
+
+ elif re.search('film', fn):
+ use = 'copper'
+
+ elif re.search('out(line)?', fn):
+ use = 'mechanical'
+ side = 'outline'
+
+ return f'{side} {use}'
class LayerStack:
@classmethod
@@ -160,7 +170,7 @@ class LayerStack:
if len(filemap) < 6:
raise ValueError('Cannot figure out gerber file mapping. Partial map is: ', filemap)
- elif generator == 'geda':
+ if generator == 'geda':
# geda is written by geniuses who waste no bytes of unnecessary output so it doesn't actually include the
# number format in files that use imperial units. Unfortunately it also doesn't include any hints that the
# file was generated by geda, so we have to guess by context whether this is just geda being geda or
@@ -182,20 +192,39 @@ class LayerStack:
raise SystemError('Cannot figure out gerber file mapping')
# FIXME use layer metadata from comments and ipc file if available
+ elif generator == 'altium':
+ excellon_settings = None
+
+ if 'mechanical outline' in filemap:
+ # Use lowest-numbered mechanical layer as outline, ignore others.
+ mechs = {}
+ for layer in filemap['mechanical outline']:
+ if layer.name.lower().endswith('gko'):
+ filemap['mechanical outline'] = [layer]
+ break
+
+ if (m := re.match(r'.*\.gm([0-9]+)', layer.name, re.IGNORECASE)):
+ mechs[int(m[1])] = layer
+ else:
+ break
+ else:
+ filemap['mechanical outline'] = [sorted(mechs.items(), key=lambda x: x[0])[0][1]]
+
else:
excellon_settings = None
- if any(len(value) > 1 for value in filemap.values()):
- raise SystemError('Ambgiuous layer names')
+ ambiguous = [ key for key, value in filemap.items() if len(value) > 1 and not 'drill' in key ]
+ if ambiguous:
+ raise SystemError(f'Ambiguous layer names for {", ".join(ambiguous)}')
drill_layers = []
- layers = { key: None for key in STANDARD_LAYERS }
+ layers = {} # { tuple(key.split()): None for key in STANDARD_LAYERS }
for key, paths in filemap.items():
if len(paths) > 1 and not 'drill' in key:
raise ValueError(f'Multiple matching files found for {key} layer: {", ".join(value)}')
for path in paths:
- if 'outline' in key or 'drill' in key and identify_file(path.read_text()) != 'gerber':
+ if ('outline' in key or 'drill' in key) and identify_file(path.read_text()) != 'gerber':
if 'nonplated' in key:
plated = False
elif 'plated' in key:
@@ -206,8 +235,8 @@ class LayerStack:
else:
layer = GerberFile.open(path)
- if key == 'drill outline':
- layers['outline'] = layer
+ if key == 'mechanical outline':
+ layers['mechanical', 'outline'] = layer
elif 'drill' in key:
drill_layers.append(layer)
@@ -221,9 +250,9 @@ class LayerStack:
warnings.warn('File identification returned ambiguous results. Please raise an issue on the gerbonara '
'tracker and if possible please provide these input files for reference.')
- board_name = common_prefix([f.name for f in filemap.values()])
- board_name = re.subs(r'^\W+', '', board_name)
- board_name = re.subs(r'\W+$', '', board_name)
+ board_name = common_prefix([l.original_path.name for l in layers.values() if l is not None])
+ board_name = re.sub(r'^\W+', '', board_name)
+ board_name = re.sub(r'\W+$', '', board_name)
return kls(layers, drill_layers, board_name=board_name)
def __init__(self, graphic_layers, drill_layers, board_name=None):
@@ -231,6 +260,13 @@ class LayerStack:
self.drill_layers = drill_layers
self.board_name = board_name
+ def __str__(self):
+ names = [ f'{side} {use}' for side, use in self.graphic_layers ]
+ return f'<LayerStack {self.board_name} [{", ".join(names)}] and {len(self.drill_layers)} drill layers>'
+
+ def __repr__(self):
+ return str(self)
+
def merge_drill_layers(self):
target = ExcellonFile(comments='Drill files merged by gerbonara')
@@ -283,7 +319,9 @@ class LayerStack:
def drill_layers(self):
if self._drill_layers:
return self._drill_layers
- return [self.drill_pth, self.drill_npth, self.drill_unknown]
+ if self.drill_pth or self.drill_npth or self.drill_unknown:
+ return [self.drill_pth, self.drill_npth, self.drill_unknown]
+ return []
@drill_layers.setter
def drill_layers(self, value):
@@ -305,17 +343,17 @@ class LayerStack:
return (side, use) in self.layers
elif isinstance(index, tuple):
- return index in self.layers
+ return index in self.graphic_layers
return index < len(self.copper_layers)
def __getitem__(self, index):
if isinstance(index, str):
side, _, use = index.partition(' ')
- return self.layers[(side, use)]
+ return self.graphic_layers[(side, use)]
elif isinstance(index, tuple):
- return self.layers[index]
+ return self.graphic_layers[index]
return self.copper_layers[index]
@@ -336,15 +374,15 @@ class LayerStack:
@property
def top_side(self):
- return { key: self[key] for key in ('top copper', 'top mask', 'top silk', 'top paste', 'outline') }
+ return { key: self[key] for key in ('top copper', 'top mask', 'top silk', 'top paste', 'mechanical outline') }
@property
def bottom_side(self):
- return { key: self[key] for key in ('bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'outline') }
+ return { key: self[key] for key in ('bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'mechanical outline') }
@property
def outline(self):
- return self['outline']
+ return self['mechanical outline']
def _merge_layer(self, target, source):
if source is None:
@@ -392,7 +430,7 @@ class LayerStack:
for i, layer in enumerate(new_inner, start=1):
self[f'inner_{i} copper'] = layer
- self._merge_layer('outline', other['outline'])
+ self._merge_layer('mechanical outline', other['mechanical outline'])
self.normalize_drill_layers()
other.normalize_drill_layers()
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py
index 5973ead..07dbac4 100644
--- a/gerbonara/gerber/rs274x.py
+++ b/gerbonara/gerber/rs274x.py
@@ -53,9 +53,9 @@ class GerberFile(CamFile):
The GerberFile class represents a single gerber file.
"""
- def __init__(self, objects=None, comments=None, import_settings=None, filename=None, generator_hints=None,
+ def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None,
layer_hints=None, file_attrs=None):
- super().__init__(filename=filename)
+ super().__init__(original_path=original_path)
self.objects = objects or []
self.comments = comments or []
self.generator_hints = generator_hints or []
@@ -157,7 +157,7 @@ class GerberFile(CamFile):
with open(filename, "r") as f:
if enable_includes and enable_include_dir is None:
enable_include_dir = filename.parent
- return kls.from_string(f.read(), enable_include_dir, filename=filename.name)
+ return kls.from_string(f.read(), enable_include_dir, filename=filename)
@classmethod
def from_string(kls, data, enable_include_dir=None, filename=None):
@@ -217,7 +217,8 @@ class GerberFile(CamFile):
yield 'M02*'
def __str__(self):
- return f'<GerberFile with {len(self.apertures)} apertures, {len(self.objects)} objects>'
+ name = f'{self.original_path.name} ' if self.original_path else ''
+ return f'<GerberFile {name}with {len(self.apertures)} apertures, {len(self.objects)} objects>'
def save(self, filename, settings=None, drop_comments=True):
with open(filename, 'w', encoding='utf-8') as f: # Encoding is specified as UTF-8 by spec.
@@ -637,6 +638,7 @@ class GerberParser:
self.target.import_settings = self.file_settings
self.target.unit = self.file_settings.unit
self.target.file_attrs = self.file_attrs
+ self.target.original_path = filename
if not self.eof_found:
self.warn('File is missing mandatory M02 EOF marker. File may be truncated.')
diff --git a/gerbonara/gerber/tests/test_layers.py b/gerbonara/gerber/tests/test_layers.py
index 62ef522..482d340 100644
--- a/gerbonara/gerber/tests/test_layers.py
+++ b/gerbonara/gerber/tests/test_layers.py
@@ -31,11 +31,11 @@ REFERENCE_DIRS = {
'IRNASIoTbank1.2.Bot': 'bottom copper',
'IRNASIoTbank1.2.Drill': 'drill plated',
'IRNASIoTbank1.2.Info': None,
- 'IRNASIoTbank1.2.Outline': 'drill outline',
+ 'IRNASIoTbank1.2.Outline': 'mechanical outline',
'IRNASIoTbank1.2.PasteBot': 'bottom paste',
'IRNASIoTbank1.2.PasteTop': 'top paste',
- 'IRNASIoTbank1.2.PosiBot': 'bottom silkscreen',
- 'IRNASIoTbank1.2.PosiTop': 'top silkscreen',
+ 'IRNASIoTbank1.2.PosiBot': 'bottom silk',
+ 'IRNASIoTbank1.2.PosiTop': 'top silk',
'IRNASIoTbank1.2.StopBot': 'bottom mask',
'IRNASIoTbank1.2.StopTop': 'top mask',
'IRNASIoTbank1.2.Tool': None,
@@ -45,7 +45,7 @@ REFERENCE_DIRS = {
'allegro': {
'08_057494d-ipc356.ipc': None,
- '08_057494d.rou': 'drill outline',
+ '08_057494d.rou': 'mechanical outline',
'Read_Me.1': None,
'art_param.txt': None,
'assy1.art': None,
@@ -73,7 +73,7 @@ REFERENCE_DIRS = {
'MINNOWMAX_REVA2_PUBLIC_TOPSIDE.pdf': None,
'MinnowMax_RevA1_IPC356A.ipc': None,
'MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCDRILL.drl': 'drill unknown',
- 'MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCROUTE.rou': 'drill outline',
+ 'MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCROUTE.rou': 'drill unknown',
'MinnowMax_RevA1_DRILL/nc_param.txt': None,
'MinnowMax_RevA1_DRILL/ncdrill.log': None,
'MinnowMax_RevA1_DRILL/ncroute.log': None,
@@ -116,15 +116,15 @@ REFERENCE_DIRS = {
'Gerber/LimeSDR-QPCIe_1v2.GBO': 'bottom silk',
'Gerber/LimeSDR-QPCIe_1v2.GBP': 'bottom paste',
'Gerber/LimeSDR-QPCIe_1v2.GBS': 'bottom mask',
- 'Gerber/LimeSDR-QPCIe_1v2.GM1': 'dirll outlinej',
+ 'Gerber/LimeSDR-QPCIe_1v2.GM1': 'mechanical outline',
'Gerber/LimeSDR-QPCIe_1v2.GM14': None,
'Gerber/LimeSDR-QPCIe_1v2.GM15': None,
'Gerber/LimeSDR-QPCIe_1v2.GPB': None,
'Gerber/LimeSDR-QPCIe_1v2.GPT': None,
- 'Gerber/LimeSDR-QPCIe_1v2.GTL': 'bottom copper',
- 'Gerber/LimeSDR-QPCIe_1v2.GTO': 'bottom silk',
- 'Gerber/LimeSDR-QPCIe_1v2.GTP': 'bottom paste',
- 'Gerber/LimeSDR-QPCIe_1v2.GTS': 'bottom mask',
+ 'Gerber/LimeSDR-QPCIe_1v2.GTL': 'top copper',
+ 'Gerber/LimeSDR-QPCIe_1v2.GTO': 'top silk',
+ 'Gerber/LimeSDR-QPCIe_1v2.GTP': 'top paste',
+ 'Gerber/LimeSDR-QPCIe_1v2.GTS': 'top mask',
'Gerber/LimeSDR-QPCIe_1v2.REP': None,
'Gerber/LimeSDR-QPCIe_1v2.RUL': None,
'Gerber/LimeSDR-QPCIe_1v2.apr': None,
@@ -134,22 +134,23 @@ REFERENCE_DIRS = {
'NC Drill/LimeSDR-QPCIe_1v2.LDP': None,
},
- 'diptrace': {
- 'mainboard.drl': 'drill plated',
- 'mainboard_BoardOutline.gbr': 'drill outline',
- 'mainboard_Bottom.gbr': 'bottom copper',
- 'mainboard_BottomMask.gbr': 'bottom mask',
- 'mainboard_Top.gbr': 'top copper',
- 'mainboard_TopMask.gbr': 'top mask',
- 'mainboard_TopSilk.gbr': 'top silk',
- },
+# TODO there are three designs in this folder. make test work with that.
+# 'diptrace': {
+# 'mainboard.drl': 'drill plated',
+# 'mainboard_BoardOutline.gbr': 'mechanical outline',
+# 'mainboard_Bottom.gbr': 'bottom copper',
+# 'mainboard_BottomMask.gbr': 'bottom mask',
+# 'mainboard_Top.gbr': 'top copper',
+# 'mainboard_TopMask.gbr': 'top mask',
+# 'mainboard_TopSilk.gbr': 'top silk',
+# },
'eagle-newer': {
'copper_bottom.gbr': 'bottom copper',
'copper_top.gbr': 'top copper',
'drills.xln': 'drill unknown',
'gerber_job.gbrjob': None,
- 'profile.gbr': 'drill outline',
+ 'profile.gbr': 'mechanical outline',
'silkscreen_bottom.gbr': 'bottom silk',
'silkscreen_top.gbr': 'top silk',
'soldermask_bottom.gbr': 'bottom mask',
@@ -163,7 +164,7 @@ REFERENCE_DIRS = {
'copper_inner_l2.gbr': 'inner_2 copper',
'copper_inner_l3.gbr': 'inner_3 copper',
'copper_top_l1.gbr': 'top copper',
- 'profile.gbr': 'drill outline',
+ 'profile.gbr': 'mechanical outline',
'silkscreen_bottom.gbr': 'bottom silk',
'silkscreen_top.gbr': 'top silk',
'soldermask_bottom.gbr': 'bottom mask',
@@ -173,7 +174,7 @@ REFERENCE_DIRS = {
},
'easyeda': {
- 'Gerber_BoardOutline.GKO': 'drill outline',
+ 'Gerber_BoardOutline.GKO': 'mechanical outline',
'Gerber_BottomLayer.GBL': 'bottom copper',
'Gerber_BottomSolderMaskLayer.GBS': 'bottom mask',
'Gerber_Drill_NPTH.DRL': 'drill nonplated',
@@ -190,7 +191,7 @@ REFERENCE_DIRS = {
},
'fritzing': {
- 'combined.GKO': 'drill outline',
+ 'combined.GKO': 'mechanical outline',
'combined.gbl': 'bottom copper',
'combined.gbo': 'bottom silk',
'combined.gbs': 'bottom mask',
@@ -222,7 +223,7 @@ REFERENCE_DIRS = {
'power-art.gbo': 'bottom silk',
'power-art.gbp': 'bottom paste',
'power-art.gbs': 'bottom mask',
- 'power-art.gko': 'drill outline',
+ 'power-art.gko': 'mechanical outline',
'power-art.gtl': 'top copper',
'power-art.gto': 'top silk',
'power-art.gtp': 'top paste',
@@ -232,7 +233,7 @@ REFERENCE_DIRS = {
},
'siemens': {
- '80101_0125_F200_ContourPlated.ncd': 'drill outline',
+ '80101_0125_F200_ContourPlated.ncd': 'mechanical outline',
'80101_0125_F200_DrillDrawingThrough.gdo': None,
'80101_0125_F200_L01_Top.gdo': 'top copper',
'80101_0125_F200_L02.gdo': 'inner_2 copper',
@@ -257,7 +258,7 @@ REFERENCE_DIRS = {
},
'siemens-2': {
- 'Gerber/BoardOutlline.gdo': 'drill outline',
+ 'Gerber/BoardOutlline.gdo': 'mechanical outline',
'Gerber/DrillDrawingThrough.gdo': None,
'Gerber/EtchLayerBottom.gdo': 'bottom copper',
'Gerber/EtchLayerTop.gdo': 'top copper',
@@ -267,7 +268,7 @@ REFERENCE_DIRS = {
'Gerber/SolderPasteTop.gdo': 'top paste',
'Gerber/SoldermaskBottom.gdo': 'bottom mask',
'Gerber/SoldermaskTop.gdo': 'top mask',
- 'NCDrill/ContourPlated.ncd': 'drill outline',
+ 'NCDrill/ContourPlated.ncd': 'mechanical outline',
'NCDrill/ThruHoleNonPlated.ncd': 'drill nonplated',
'NCDrill/ThruHolePlated.ncd': 'drill plated',
},
@@ -278,7 +279,7 @@ REFERENCE_DIRS = {
'design_export.gbo': 'bottom silk',
'design_export.gbp': 'bottom paste',
'design_export.gbs': 'bottom mask',
- 'design_export.gko': 'drill outline',
+ 'design_export.gko': 'mechanical outline',
'design_export.gtl': 'top copper',
'design_export.gto': 'top silk',
'design_export.gtp': 'top paste',
@@ -295,27 +296,34 @@ def test_layer_classifier(ref_dir):
path = reference_path(ref_dir)
print('Reference path is', path)
file_map = { filename: role for filename, role in file_map.items() if role is not None }
- rev_file_map = { value: key for key, value in file_map.items() }
+ rev_file_map = { tuple(value.split()): key for key, value in file_map.items() }
drill_files = { filename: role for filename, role in file_map.items() if role.startswith('drill') }
stack = LayerStack.from_directory(path)
+ print('loaded layers:', ', '.join(f'{side} {use}' for side, use in stack.graphic_layers))
for side in 'top', 'bottom':
for layer in 'copper', 'silk', 'mask', 'paste':
+ if 'allegro-2' in ref_dir and layer in ('silk', 'mask', 'paste'):
+ # This particular example has very poorly named files
+ continue
if (side, layer) in rev_file_map:
assert (side, layer) in stack
found = stack[side, layer]
assert isinstance(found, GerberFile)
- assert found.filename == Path(rev_file_map[side, layer]).name
+ assert found.original_path.name == Path(rev_file_map[side, layer]).name
else: # not in file_map
assert (side, layer) not in stack
- for filename, role in drill_files:
- assert any(layer.filename == Path(filename).name for layer in stack.drill_layers)
- assert len(stack.drill_layers) == len(drill_files)
+ assert len(stack.drill_layers) == len(drill_files)
+
+ for filename, role in drill_files.items():
+ print('drill:', filename, role)
+ print([(layer.original_path, layer.original_path == Path(filename).name) for layer in stack.drill_layers])
+ assert any(layer.original_path.name == Path(filename).name for layer in stack.drill_layers)
- for layer in stack.drill_files:
+ for layer in stack.drill_layers:
assert isinstance(layer, ExcellonFile)
diff --git a/gerbonara/gerber/utils.py b/gerbonara/gerber/utils.py
index 00ba41e..5bc03e5 100644
--- a/gerbonara/gerber/utils.py
+++ b/gerbonara/gerber/utils.py
@@ -46,7 +46,6 @@ class RegexMatcher:
def handle(self, inst, line):
for regex, handler in self.mapping.items():
if (match := re.fullmatch(regex, line)):
- #print(' handler', handler.__name__)
handler(inst, match)
return True
else: