From c3ca4f95bd59f69d45e582a4149327f57a360760 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 30 Jan 2022 20:11:38 +0100 Subject: Rename gerbonara/gerber package to just gerbonara --- gerbonara/gerber/layers.py | 482 --------------------------------------------- 1 file changed, 482 deletions(-) delete mode 100644 gerbonara/gerber/layers.py (limited to 'gerbonara/gerber/layers.py') diff --git a/gerbonara/gerber/layers.py b/gerbonara/gerber/layers.py deleted file mode 100644 index fcaf0fc..0000000 --- a/gerbonara/gerber/layers.py +++ /dev/null @@ -1,482 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright 2014 Hamilton Kibbe -# Copyright 2021 Jan Götte -# -# 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 warnings -from collections import namedtuple -from pathlib import Path - -from .excellon import ExcellonFile -from .rs274x import GerberFile -from .ipc356 import Netlist -from .cam import FileSettings -from .layer_rules import MATCH_RULES - - -STANDARD_LAYERS = [ - 'mechanical outline', - 'top copper', - 'top mask', - 'top silk', - 'top paste', - 'bottom copper', - 'bottom mask', - 'bottom silk', - 'bottom paste', - ] - - -def match_files(filenames): - matches = {} - for generator, rules in MATCH_RULES.items(): - gen = {} - matches[generator] = gen - for layer, regex in rules.items(): - for fn in filenames: - if (m := re.fullmatch(regex, fn.name, re.IGNORECASE)): - if layer == 'inner copper': - 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): - matches = match_files(filenames) - matches = sorted(matches.items(), key=lambda pair: len(pair[1])) - generator, files = matches[-1] - return generator, files - -def identify_file(data): - if 'M48' in data: - return 'excellon' - - if 'G90' in data and ';LEADER:' in data: # yet another allegro special case - return 'excellon' - - if 'FSLAX' in data or 'FSTAX' in data: - return 'gerber' - - if 'UNITS CUST' in data: - return 'ipc356' - - return None - -def common_prefix(l): - out = [] - for cand in l: - score = lambda n: sum(elem.startswith(cand[:n]) for elem in l) - baseline = score(1) - if len(l) - baseline > 5: - continue - for n in range(2, len(cand)): - if len(l) - score(n) > 5: - break - out.append(cand[:n-1]) - - if not out: - return '' - - return sorted(out, key=len)[-1] - -def autoguess(filenames): - prefix = common_prefix([f.name 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 '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('.') - - if ext in ('log', 'err'): - return 'unknown unknown' - - side, use = 'unknown', 'unknown' - if re.search('top|front|pri?m?(ary)?', fn): - side = 'top' - use = 'copper' - if re.search('bot(tom)?|back|sec(ondary)?', fn): - side = 'bottom' - use = 'copper' - - if re.search('silks?(creen)?', fn): - use = 'silk' - - elif re.search('(solder)?paste', fn): - use = 'paste' - - elif re.search('(solder)?mask', fn): - use = 'mask' - - elif re.search('drill|rout?e?', fn): - use = 'drill' - side = 'unknown' - - if re.search(r'np(th)?|(non|un)\W*plated|(non|un)\Wgalv', fn): - side = 'nonplated' - - elif re.search('pth|plated|galv', fn): - side = 'plated' - - elif (m := re.search(r'(la?y?e?r?|in(ner)?)\W*(?P[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 = 'outline' - side = 'mechanical' - - elif 'ipc' in fn and '356' in fn: - use = 'netlist' - side = 'other' - - elif 'netlist' in fn: - use = 'netlist' - side = 'other' - - return f'{side} {use}' - -class LayerStack: - @classmethod - def from_directory(kls, directory, board_name=None, verbose=False): - - directory = Path(directory) - if not directory.is_dir(): - raise FileNotFoundError(f'{directory} is not a directory') - - files = [ path for path in directory.glob('**/*') if path.is_file() ] - generator, filemap = best_match(files) - print('detected generator', generator) - - if len(filemap) < 6: - warnings.warn('Ambiguous gerber filenames. Trying last-resort autoguesser.') - generator = None - filemap = autoguess(files) - if len(filemap) < 6: - raise ValueError('Cannot figure out gerber file mapping. Partial map is: ', filemap) - - 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 - # potential user error. - excellon_settings = FileSettings(number_format=(2, 4)) - - elif generator == 'allegro': - # Allegro puts information that is absolutely vital for parsing its excellon files... into another file, - # next to the actual excellon file. Despite pretty much everyone else having figured out a way to put that - # info into the excellon file itself, even if only as a comment. - if 'excellon params' in filemap: - excellon_settings = parse_allegro_ncparam(filemap['excellon params'][0].read_text()) - del filemap['excellon params'] - # Ignore if we can't find the param file -- maybe the user has convinced Allegro to actually put this - # information into a comment, or maybe they have made Allegro just use decimal points like XNC does. - - filemap = autoguess([ f for files in filemap for f in files ]) - if len(filemap < 6): - 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 - - import pprint - pprint.pprint(filemap) - - 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 = [] - netlist = None - 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: - id_result = identify_file(path.read_text()) - print('id_result', id_result) - - if 'netlist' in key: - layer = Netlist.open(path) - - elif ('outline' in key or 'drill' in key) and id_result != 'gerber': - if id_result is None: - # Since e.g. altium uses ".txt" as the extension for its drill files, we have to assume the - # current file might not be a drill file after all. - continue - - if 'nonplated' in key: - plated = False - elif 'plated' in key: - plated = True - else: - plated = None - layer = ExcellonFile.open(path, plated=plated, settings=excellon_settings) - else: - - layer = GerberFile.open(path) - - if key == 'mechanical outline': - layers['mechanical', 'outline'] = layer - - elif 'drill' in key: - drill_layers.append(layer) - - elif 'netlist' in key: - if netlist: - warnings.warn(f'Found multiple netlist files, using only first one. Have: {netlist.original_path.name}, got {path.name}') - else: - netlist = layer - - else: - side, _, use = key.partition(' ') - layers[(side, use)] = layer - - hints = set(layer.generator_hints) | { generator } - if len(hints) > 1: - 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([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, netlist, board_name=board_name) - - def __init__(self, graphic_layers, drill_layers, netlist=None, board_name=None): - self.graphic_layers = graphic_layers - self.drill_layers = drill_layers - self.board_name = board_name - self.netlist = netlist - - def __str__(self): - names = [ f'{side} {use}' for side, use in self.graphic_layers ] - return f'' - - def __repr__(self): - return str(self) - - def merge_drill_layers(self): - target = ExcellonFile(comments='Drill files merged by gerbonara') - - for layer in self.drill_layers: - if isinstance(layer, GerberFile): - layer = layer.to_excellon() - - target.merge(layer) - - self.drill_layers = [target] - - def normalize_drill_layers(self): - # TODO: maybe also separate into drill and route? - drill_pth, drill_npth, drill_aux = [], [], [] - - for layer in self.drill_layers: - if isinstance(layer, GerberFile): - layer = layer.to_excellon() - - if layer.is_plated: - drill_pth.append(layer) - elif layer.is_nonplated: - drill_pth.append(layer) - else: - drill_aux.append(layer) - - pth_out, *rest = drill_pth or [ExcellonFile()] - for layer in rest: - pth_out.merge(layer) - - npth_out, *rest = drill_npth or [ExcellonFile()] - for layer in rest: - npth_out.merge(layer) - - unknown_out = ExcellonFile() - for layer in drill_aux: - for obj in layer.objects: - if obj.plated is None: - unknown_out.append(obj) - elif obj.plated: - pth_out.append(obj) - else: - npth_out.append(obj) - - self.drill_pth, self.drill_npth = pth_out, npth_out - self.drill_unknown = unknown_out if unknown_out else None - self._drill_layers = [] - - @property - def drill_layers(self): - if self._drill_layers: - return self._drill_layers - 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): - self._drill_layers = value - self.drill_pth = self.drill_npth = self.drill_unknown = None - - def __len__(self): - return len(self.layers) - - def get(self, index, default=None): - if self.contains(key): - return self[key] - else: - return default - - def __contains__(self, index): - if isinstance(index, str): - side, _, use = index.partition(' ') - return (side, use) in self.layers - - elif isinstance(index, tuple): - 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.graphic_layers[(side, use)] - - elif isinstance(index, tuple): - return self.graphic_layers[index] - - return self.copper_layers[index] - - @property - def copper_layers(self): - copper_layers = [ (key, layer) for key, layer in self.layers.items() if key.endswith('copper') ] - - def sort_layername(val): - key, _layer = val - if key.startswith('top'): - return -1 - if key.startswith('bottom'): - return 1e99 - assert key.startswith('inner_') - return int(key[len('inner_'):]) - - return [ layer for _key, layer in sorted(copper_layers, key=sort_layername) ] - - @property - def top_side(self): - 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', 'mechanical outline') } - - @property - def outline(self): - return self['mechanical outline'] - - def _merge_layer(self, target, source): - if source is None: - return - - if self[target] is None: - self[target] = source - - else: - self[target].merge(source) - - def merge(self, other): - all_keys = set(self.layers.keys()) | set(other.layers.keys()) - exclude = { key.split() for key in STANDARD_LAYERS } - all_keys = { key for key in all_keys if key not in exclude } - if all_keys: - warnings.warn('Cannot merge unknown layer types: {" ".join(all_keys)}') - - for side in 'top', 'bottom': - for use in 'copper', 'mask', 'silk', 'paste': - self._merge_layer((side, use), other[side, use]) - - our_inner, their_inner = self.copper_layers[1:-1], other.copper_layers[1:-1] - - if bool(our_inner) != bool(their_inner): - warnings.warn('Merging board without inner layers into board with inner layers, inner layers will be empty on first board.') - - elif our_inner and their_inner: - warnings.warn('Merging boards with different inner layer counts. Will fill inner layers starting at core.') - - diff = len(our_inner) - len(their_inner) - their_inner = ([None] * max(0, diff//2)) + their_inner + ([None] * max(0, diff//2)) - our_inner = ([None] * max(0, -diff//2)) + their_inner + ([None] * max(0, -diff//2)) - - new_inner = [] - for ours, theirs in zip(our_inner, their_inner): - if ours is None: - new_inner.append(theirs) - elif theirs is None: - new_inner.append(ours) - else: - ours.merge(theirs) - new_inner.append(ours) - - for i, layer in enumerate(new_inner, start=1): - self[f'inner_{i} copper'] = layer - - self._merge_layer('mechanical outline', other['mechanical outline']) - - self.normalize_drill_layers() - other.normalize_drill_layers() - - self.drill_pth.merge(other.drill_pth) - self.drill_npth.merge(other.drill_npth) - self.drill_unknown.merge(other.drill_unknown) - self.netlist.merge(other.netlist) - -- cgit