From a88364b7a98b6f8d8edd4f1616908ba8889485dd Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 18 Jun 2022 23:51:43 +0200 Subject: Fix up saving and zip writing logic --- gerbonara/excellon.py | 8 ++-- gerbonara/ipc356.py | 8 ++-- gerbonara/layers.py | 108 ++++++++++++++++++++++---------------------------- gerbonara/rs274x.py | 18 ++++----- 4 files changed, 64 insertions(+), 78 deletions(-) diff --git a/gerbonara/excellon.py b/gerbonara/excellon.py index 54fcb51..ea5eba4 100755 --- a/gerbonara/excellon.py +++ b/gerbonara/excellon.py @@ -378,7 +378,7 @@ class ExcellonFile(CamFile): yield 'M30' - def generate_excellon(self, settings=None, drop_comments=True): + def write_to_bytes(self, settings=None, drop_comments=True): """ Export to Excellon format. This function always generates XNC, which is a well-defined subset of Excellon. Uses sane default settings if you don't give any. @@ -397,13 +397,13 @@ class ExcellonFile(CamFile): settings = FileSettings() settings.zeros = None settings.number_format = (3,5) - return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)) + return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)).encode('utf-8') def save(self, filename, settings=None, drop_comments=True): """ Save this Excellon file to the file system. See :py:meth:`~.ExcellonFile.generate_excellon` for the meaning of the arguments. """ - with open(filename, 'w') as f: - f.write(self.generate_excellon(settings, drop_comments=drop_comments)) + with open(filename, 'wb') as f: + f.write(self.write_to_bytes(settings, drop_comments=drop_comments)) def offset(self, x=0, y=0, unit=MM): for obj in self.objects: diff --git a/gerbonara/ipc356.py b/gerbonara/ipc356.py index ae341be..06a87f0 100644 --- a/gerbonara/ipc356.py +++ b/gerbonara/ipc356.py @@ -120,15 +120,15 @@ class Netlist(CamFile): return parser.parse(data, Path(filename)) def save(self, filename, settings=None, drop_comments=True): - with open(filename, 'w', encoding='utf-8') as f: - f.write(self.to_ipc356(settings, drop_comments=drop_comments)) + with open(filename, 'wb') as f: + f.write(self.write_to_bytes(settings, drop_comments=drop_comments)) - def to_ipc356(self, settings=None, drop_comments=True, job_name=None): + def write_to_bytes(self, settings=None, drop_comments=True, job_name=None): if settings is None: settings = self.import_settings.copy() or FileSettings() settings.zeros = None settings.number_format = (5,6) - return '\n'.join(self._generate_lines(settings, drop_comments=drop_comments)) + return '\n'.join(self._generate_lines(settings, drop_comments=drop_comments)).encode('utf-8') def _generate_lines(self, settings, drop_comments, job_name=None): yield 'C IPC-D-356 generated by Gerbonara' diff --git a/gerbonara/layers.py b/gerbonara/layers.py index 4fcc6e3..75997be 100644 --- a/gerbonara/layers.py +++ b/gerbonara/layers.py @@ -18,6 +18,8 @@ # import os +import io +import sys import re import warnings import copy @@ -213,31 +215,35 @@ class LayerStack: @classmethod def open(kls, path, board_name=None, lazy=False): + if str(path) == '-': + data_io = io.BytesIO(sys.stdin.buffer.read()) + return kls.from_zip_data(data_io, original_path='', board_name=board_name, lazy=lazy) + path = Path(path) if path.is_dir(): - return kls.from_directory(path, board_name=board_name, lazy=lazy) + return kls.open_dir(path, board_name=board_name, lazy=lazy) elif path.suffix.lower() == '.zip' or is_zipfile(path): - return kls.from_zipfile(path, board_name=board_name, lazy=lazy) + return kls.open_zip(path, board_name=board_name, lazy=lazy) else: return kls.from_files([path], board_name=board_name, lazy=lazy) @classmethod - def from_zipfile(kls, filename, board_name=None, lazy=False): + def open_zip(kls, file, original_path=None, board_name=None, lazy=False): tmpdir = tempfile.TemporaryDirectory() - tmp_indir = Path(tmpdir) / dirname + tmp_indir = Path(tmpdir) / 'input' tmp_indir.mkdir() - with ZipFile(source) as f: + with ZipFile(file) as f: f.extractall(path=tmp_indir) inst = kls.from_directory(tmp_indir, board_name=board_name, lazy=lazy) inst.tmpdir = tmpdir - inst.original_path = filename + inst.original_path = Path(original_path or file) inst.was_zipped = True return inst @classmethod - def from_directory(kls, directory, board_name=None, lazy=False): + def open_dir(kls, directory, board_name=None, lazy=False): directory = Path(directory) if not directory.is_dir(): @@ -369,30 +375,39 @@ 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([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) + if not 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, netlist, board_name=board_name, original_path=original_path, was_zipped=was_zipped) - def save_to_zipfile(self, path, naming_scheme={}): - with tempfile.TemporaryDirectory() as tempdir: - self.save_to_directory(path, naming_scheme=naming_scheme) - with ZipFile(path) as le_zip: - for f in Path(tempdir.name).glob('*'): - with le_zip.open(f, 'wb') as out: - out.write(f.read_bytes()) + def save_to_zipfile(self, path, naming_scheme={}, overwrite_existing=True, prefix=''): + if path.is_file(): + if overwrite_existing: + path.unlink() + else: + raise ValueError('output zip file already exists and overwrite_existing is False') + + with ZipFile(path) as le_zip: + for path, layer in self._save_files_iter(naming_scheme=naming_scheme): + with le_zip.open(prefix + str(path), 'wb') as out: + out.write(layer.write_to_bytes()) def save_to_directory(self, path, naming_scheme={}, overwrite_existing=True): outdir = Path(path) outdir.mkdir(parents=True, exist_ok=overwrite_existing) - def check_not_exists(path): - if path.exists() and not overwrite_existing: - raise SystemError(f'Path exists but overwrite_existing is False: {path}') + for path, layer in self._save_files_iter(naming_scheme=naming_scheme): + out = outdir / path + if out.exists() and not overwrite_existing: + raise SystemError(f'Path exists but overwrite_existing is False: {out}') + layer.save(out) + def _save_files_iter(self, naming_scheme={}): def get_name(layer_type, layer): - nonlocal naming_scheme, overwrite_existing + nonlocal naming_scheme if (m := re.match('inner_([0-9]*) copper', layer_type)): layer_type = 'inner copper' @@ -401,55 +416,26 @@ class LayerStack: num = None if layer_type in naming_scheme: - path = outdir / naming_scheme[layer_type].format(layer_num=num, board_name=self.board_name) + path = naming_scheme[layer_type].format(layer_number=num, board_name=self.board_name) else: - path = outdir / layer.original_path.name + path = layer.original_path.name - check_not_exists(path) return path for (side, use), layer in self.graphic_layers.items(): - outpath = get_name(f'{side} {use}', layer) - layer.save(outpath) - - if naming_scheme: - self.normalize_drill_layers() - - def save_layer(layer, layer_name): - nonlocal self, outdir, drill_layers, check_not_exists - path = outdir / drill_layers[layer_name].format(board_name=self.board_name) - check_not_exists(path) - layer.save(path) - - drill_layers = { key.partition()[2]: value for key, value in naming_scheme if 'drill' in key } - if set(drill_layers) == {'plated', 'nonplated', 'unknown'}: - save_layer(self.drill_pth, 'plated') - save_layer(self.drill_npth, 'nonplated') - save_layer(self.drill_unknown, 'unknown') - - elif 'plated' in drill_layers and len(drill_layers) == 2: - save_layer(self.drill_pth, 'plated') - merged = copy.copy(self.drill_npth) - merged.merge(self.drill_unknown) - save_layer(merged, list(set(drill_layers) - {'plated'})[0]) - - elif 'unknown' in drill_layers: - merged = copy.copy(self.drill_pth) - merged.merge(self.drill_npth) - merged.merge(self.drill_unknown) - save_layer(merged, 'unknown') + yield get_name(f'{side} {use}', layer), layer - else: - raise ValueError('Namin scheme does not specify unknown drill layer') + #self.normalize_drill_layers() - else: - for layer in self.drill_layers: - outpath = outdir / layer.original_path.name - check_not_exists(outpath) - layer.save(outpath) + if self.drill_pth is not None: + yield get_name('plated drill', self.drill_pth), self.drill_pth + if self.drill_npth is not None: + yield get_name('nonplated drill', self.drill_npth), self.drill_npth + if self.drill_unknown is not None: + yield get_name('unknown drill', self.drill_unknown), self.drill_unknown if self.netlist: - layer.save(get_name('other netlist', self.netlist)) + yield get_name('other netlist', self.netlist), self.netlist def __str__(self): names = [ f'{side} {use}' for side, use in self.graphic_layers ] diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py index 1d18ec3..7be55cc 100644 --- a/gerbonara/rs274x.py +++ b/gerbonara/rs274x.py @@ -59,17 +59,17 @@ class GerberFile(CamFile): self.apertures = [] # FIXME get rid of this? apertures are already in the objects. self.file_attrs = file_attrs or {} - def to_excellon(self): + def to_excellon(self, plated=None): new_objs = [] new_tools = {} for obj in self.objects: - if not isinstance(obj, Line) or isinstance(obj, Arc) or isinstance(obj, Flash) or \ - not isinstance(obj.aperture, CircleAperture): - raise ValueError('Cannot convert {type(obj)} to excellon!') + if not isinstance(obj, go.Line) or isinstance(obj, go.Arc) or isinstance(obj, go.Flash) or \ + not isinstance(obj.aperture, apertures.CircleAperture): + raise ValueError(f'Cannot convert {type(obj)} to excellon!') if not (new_tool := new_tools.get(id(obj.aperture))): # TODO plating? - new_tool = new_tools[id(obj.aperture)] = ExcellonTool(obj.aperture.diameter) + new_tool = new_tools[id(obj.aperture)] = apertures.ExcellonTool(obj.aperture.diameter, plated=plated) new_obj = dataclasses.replace(obj, aperture=new_tool) return ExcellonFile(objects=new_objs, comments=self.comments) @@ -245,10 +245,10 @@ class GerberFile(CamFile): def save(self, filename, settings=None, drop_comments=True): """ Save this Gerber file to the file system. See :py:meth:`~.GerberFile.generate_gerber` for the meaning of the arguments. """ - with open(filename, 'w', encoding='utf-8') as f: # Encoding is specified as UTF-8 by spec. - f.write(self.generate_gerber(settings, drop_comments=drop_comments)) + with open(filename, 'wb') as f: # Encoding is specified as UTF-8 by spec. + f.write(self.write_to_bytes(settings, drop_comments=drop_comments)) - def generate_gerber(self, settings=None, drop_comments=True): + def write_to_bytes(self, settings=None, drop_comments=True): """ Export to Gerber format. Uses either the file's original settings or sane default settings if you don't give any. @@ -263,7 +263,7 @@ class GerberFile(CamFile): settings = self.import_settings.copy() or FileSettings() settings.zeros = None settings.number_format = (5,6) - return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)) + return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments)).encode('utf-8') def __len__(self): return len(self.objects) -- cgit