#! /usr/bin/env python # -*- coding: utf-8 -*- # copyright 2014 Hamilton Kibbe # # 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. from dataclasses import dataclass @dataclass class FileSettings: output_axes : str = 'AXBY' # For deprecated AS statement image_polarity : str = 'positive' image_rotation: int = 0 mirror_image : tuple = (False, False) offset : tuple = (0, 0) scale_factor : tuple = (1.0, 1.0) # For deprecated SF statement notation : str = 'absolute' units : str = 'inch' angle_units : str = 'degrees' zeros : bool = None number_format : tuple = (2, 5) # input validation def __setattr__(self, name, value): if name == 'output_axes' and value not in [None, 'AXBY', 'AYBX']: raise ValueError('output_axes must be either "AXBY", "AYBX" or None') if name == 'image_rotation' and value not in [0, 90, 180, 270]: raise ValueError('image_rotation must be 0, 90, 180 or 270') elif name == 'image_polarity' and value not in ['positive', 'negative']: raise ValueError('image_polarity must be either "positive" or "negative"') elif name == 'mirror_image' and len(value) != 2: raise ValueError('mirror_image must be 2-tuple of bools: (mirror_a, mirror_b)') elif name == 'offset' and len(value) != 2: raise ValueError('offset must be 2-tuple of floats: (offset_a, offset_b)') elif name == 'scale_factor' and len(value) != 2: raise ValueError('scale_factor must be 2-tuple of floats: (scale_a, scale_b)') elif name == 'notation' and value not in ['inch', 'mm']: raise ValueError('Units must be either "inch" or "mm"') elif name == 'units' and value not in ['absolute', 'incremental']: raise ValueError('Notation must be either "absolute" or "incremental"') elif name == 'angle_units' and value not in ('degrees', 'radians'): raise ValueError('Angle units may be "degrees" or "radians"') elif name == 'zeros' and value not in [None, 'leading', 'trailing']: raise ValueError('zero_suppression must be either "leading" or "trailing" or None') elif name == 'number_format' and len(value) != 2: raise ValueError('Number format must be a (integer, fractional) tuple of integers') super().__setattr__(name, value) def __str__(self): return f'' class CamFile(object): """ Base class for Gerber/Excellon files. Provides a common set of settings parameters. Parameters ---------- settings : FileSettings The current file configuration. primitives : iterable List of primitives in the file. filename : string Name of the file that this CamFile represents. layer_name : string Name of the PCB layer that the file represents Attributes ---------- settings : FileSettings File settings as a FileSettings object notation : string File notation setting. May be either 'absolute' or 'incremental' units : string File units setting. May be 'inch' or 'mm' zero_suppression : string File zero-suppression setting. May be either 'leading' or 'trailling' format : tuple (, ) File decimal representation format as a tuple of (integer digits, decimal digits) """ def __init__(self, statements=None, settings=None, primitives=None, filename=None, layer_name=None): if settings is not None: self.notation = settings['notation'] self.units = settings['units'] self.zero_suppression = settings['zero_suppression'] self.zeros = settings['zeros'] self.format = settings['format'] else: self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' self.zeros = 'leading' self.format = (2, 5) self.statements = statements if statements is not None else [] if primitives is not None: self.primitives = primitives self.filename = filename self.layer_name = layer_name @property def settings(self): """ File settings Returns ------- settings : FileSettings (dict-like) A FileSettings object with the specified configuration. """ return FileSettings(self.notation, self.units, self.zero_suppression, self.format) @property def bounds(self): """ File boundaries """ pass @property def bounding_box(self): pass def to_inch(self): pass def to_metric(self): pass def render(self, ctx=None, invert=False, filename=None): """ Generate image of layer. Parameters ---------- ctx : :class:`GerberContext` GerberContext subclass used for rendering the image filename : string If provided, save the rendered image to `filename` """ if ctx is None: from .render import GerberCairoContext ctx = GerberCairoContext() ctx.set_bounds(self.bounding_box) ctx.paint_background() ctx.invert = invert ctx.new_render_layer() for p in self.primitives: ctx.render(p) ctx.flatten() if filename is not None: ctx.dump(filename)