modern_python
jaseg 2023-02-25 19:43:54 +01:00
rodzic 8b40d15dab
commit 1aaac3936f
4 zmienionych plików z 321 dodań i 14 usunięć

Wyświetl plik

@ -115,21 +115,258 @@ Modification
``gerbonara rewrite``
*********************
.. program:: gerbonara rewrite
.. code-block:: console
gerbonara rewrite [OPTIONS] INFILE OUTFILE
Parse a single gerber file, apply transformations, and re-serialize it into a new gerber file. Without transformations,
this command can be used to convert a gerber file to use different settings (e.g. units, precision), but can also be
used to "normalize" gerber files in a weird format into a more standards-compatible one as gerbonara's gerber parser is
significantly more robust for weird inputs than others.
.. option:: --warnings <default|ignore|once>
Enable or disable file format warnings during parsing (default: on)
.. option:: -t, --transform <code>
Execute python transformation script on input. You have access to the functions ``translate(x, y)``,
``scale(factor)`` and ``rotate(angle, center_x?, center_y?)``, the bounding box variables ``x_min``, ``y_min``,
``x_max``, ``y_max``, ``width`` and ``height``, and everything from python's built-in math module (e.g. ``pi``,
``sqrt``, ``sin``). As convenience methods, ``center()`` and ``origin()`` are provided to center the board
respectively move its bottom-left corner to the origin. Coordinates are given in ``--command-line-units``, angles in
degrees, and scale as a scale factor (as opposed to a percentage). Example: ``translate(-10, 0); rotate(45, 0, 5)``
.. option:: --command-line-units <metric|us-customary>
Units for values given in other options. Default: millimeter
.. option:: -n, --number-format <decimal.fractional>
Override number format to use during export in ``[integer digits].[decimal digits]`` notation, e.g. ``2.6``.
.. option:: -u, --units <metric|us-customary>
Override export file units
.. option:: -z, --zero-suppression <off|leading|trailing>
Override export zero suppression setting. Note: The meaning of this value is like in the Gerber spec for both Gerber
and Excellon files!
.. option:: --keep-comments, --drop-comments
Keep gerber comments. Note: Comments will be prepended to the start of file, and will not occur in their old
position.
.. option:: --default-settings, --reuse-input-settings
Use sensible defaults for the output file format settings (default) or use the same export settings as the input file
instead of sensible defaults.
.. option:: --input-number-format <decimal.fractional>
Override number format of input file (mostly useful for Excellon files)
.. option:: --input-units <metric|us-customary>
Override units of input file
.. option:: --input-zero-suppression <off|leading|trailing>
Override zero suppression setting of input file
``gerbonara transform``
***********************
.. program:: gerbonara transform
.. code-block:: console
gerbonara transform [OPTIONS] TRANSFORM INPATH OUTPATH
Transform all gerber files in a given directory or zip file using the given python transformation script.
In the python transformation script you have access to the functions ``translate(x, y)``, ``scale(factor)`` and
``rotate(angle, center_x?, center_y?)``, the bounding box variables ``x_min``, ``y_min``, ``x_max``, ``y_max``,
``width`` and ``height``, and everything from python's built-in math module (e.g. ``pi``, ``sqrt``, ``sin``). As
convenience methods, ``center()`` and ``origin()`` are provided to center the board resp. move its bottom-left corner to
the origin. Coordinates are given in --command-line-units, angles in degrees, and scale as a scale factor (as opposed to
a percentage). Example: ``translate(-10, 0); rotate(45, 0, 5)``
.. option:: -m, --input-map <json_file>
Extend or override layer name mapping with name map from JSON file. The JSON file must contain a single JSON dict
with an arbitrary number of string: string entries. The keys are interpreted as regexes applied to the filenames via
re.fullmatch, and each value must either be the string ``ignore`` to remove this layer from previous automatic
guesses, or a gerbonara layer name such as ``top copper``, ``inner_2 copper`` or ``bottom silk``.
.. option:: --use-builtin-name-rules, --no-builtin-name-rules
Disable built-in layer name rules and use only rules given by ``--input-map``
.. option:: --warnings <default|ignore|once>
Enable or disable file format warnings during parsing (default: on)
.. option:: --units <metric|us-customary>
Units for values given in other options. Default: millimeter
.. option:: -n, --number-format <decimal.fractional>
Override number format to use during export in ``[integer digits].[decimal digits]`` notation, e.g. ``2.6``.
.. option:: --default-settings, --reuse-input-settings
Use sensible defaults for the output file format settings (default) or use the same export settings as the input file
instead of sensible defaults.
.. option:: --force-zip
Force treating input path as a zip file (default: guess file type from extension and contents)
.. option:: --output-naming-scheme <altium|kicad>
Name output files according to the selected naming scheme instead of keeping the old file names.
``gerbonara merge``
*******************
.. program:: gerbonara merge
.. code-block:: console
$ gerbonara merge [OPTIONS] [INPATH]... OUTPATH
Merge multiple single Gerber or Excellon files, or multiple stacks of Gerber files, into one.
.. note::
When used with only one input, this command *normalizes* the input, converting all files to a well-defined, widely
supported Gerber subset with sane settings. When a ``--output-naming-scheme`` is given, it additionally renames all
files to a standardized naming convention.
.. option:: --command-line-units <metric|us-customary>
Units for values given in --transform. Default: millimeter
.. option:: --warnings <default|ignore|once>
Enable or disable file format warnings during parsing (default: on)
.. option:: --offset <COORDINATE>
Offset for the n'th file as a ``x,y`` string in unit given by ``--command-line-units`` (default: millimeter). Can be
given multiple times, and the first option affects the first input, the second option affects the second input, and
so on.
.. option:: --rotation <ROTATION>
Rotation for the n'th file in degrees clockwise, optionally followed by comma- separated rotation center X and Y
coordinates. Can be given multiple times, and the first option affects the first input, the second option affects the
second input, and so on.
.. option:: -m, --input-map <json_file>
Extend or override layer name mapping with name map from JSON file. This option can be given multiple times, in which
case the n'th option affects only the n'th input, like with ``--offset`` and ``--rotation``. The JSON file must
contain a single JSON dict with an arbitrary number of string: string entries. The keys are interpreted as regexes
applied to the filenames via re.fullmatch, and each value must either be the string "ignore" to remove this layer
from previous automatic guesses, or a gerbonara layer name such as ``top copper``, ``inner_2 copper`` or ``bottom
silk``.
.. option:: --default-settings, --reuse-input-settings
Use sensible defaults for the output file format settings (default) or use the same export settings as the input file
instead of sensible defaults.
.. option:: --output-naming-scheme <altium|kicad>
Name output files according to the selected naming scheme instead of keeping the old file names of the first input.
.. option:: --output-board-name <TEXT>
Override board name used with ``--output-naming-scheme``
.. option:: --use-builtin-name-rules, --no-builtin-name-rules
Disable built-in layer name rules and use only rules given by --input-map
File analysis
~~~~~~~~~~~~~
``gerbonara bounding-box``
**************************
.. program:: gerbonara bounding-box
.. code-block:: console
gerbonara bounding-box [OPTIONS] INFILE
Print the bounding box of a gerber file in ``[x_min] [y_min] [x_max] [y_max]`` format. The bounding box contains all
graphic objects in this file, so e.g. a 100 mm by 100 mm square drawn with a 1mm width circular aperture will result in
an 101 mm by 101 mm bounding box.
.. option:: --warnings <default|ignore|once>
Enable or disable file format warnings during parsing (default: on)
.. option:: --units <metric|us-customary>
Output bounding box in this unit (default: millimeter)
.. option:: --input-number-format <decimal.fractional>
Override number format of input file (mostly useful for Excellon files)
.. option:: --input-units <metric|us-customary>
Override units of input file
.. option:: --input-zero-suppression <off|leading|trailing>
Override zero suppression setting of input file
``gerbonara meta``
******************
.. program:: gerbonara meta
.. code-block:: console
gerbonara meta [OPTIONS] PATH
Read a board from a folder or zip, and print the found layer mapping along with layer metadata as JSON to stdout. A
machine-readable variant of the :program:`gerbonara render` command. All lengths in the JSON are given in millimeter.
.. option:: --warnings <default|ignore|once>
Enable or disable file format warnings during parsing (default: on)
.. option:: --force-zip
Force treating input path as zip file (default: guess file type from extension and contents)
``gerbonara layers``
********************
.. program:: gerbonara render
.. code-block:: console
$ gerbonara layers [OPTIONS] PATH
Prints a layer-by-layer description of the board found under the given path. The path can be a directory or zip file.
.. option:: --warnings <default|ignore|once>
Enable or disable file format warnings during parsing (default: on)
.. option:: --force-zip
Force treating input path as zip file (default: guess file type from extension and contents)

Wyświetl plik

@ -357,7 +357,7 @@ class CamFile:
def merge(self, other):
""" Merge ``other`` into ``self``, i.e. add all objects that are in ``other`` to ``self``. This resets
:py:attr:`.import_settings` and :py:attr:`~.CamFile.generator`. Units and other file-specific settings are
automatically handled.
handled automatically.
"""
raise NotImplementedError()

Wyświetl plik

@ -247,9 +247,10 @@ class LayerStack:
""" :py:class:`LayerStack` represents a set of Gerber files that describe different layers of the same board.
:ivar graphic_layers: :py:obj:`dict` mapping :py:obj:`(side, use)` tuples to the Gerber layers of the board.
:py:obj:`side` can be one of :py:obj:`"top"` or :py:obj:`"bottom"`, or a numbered internal
layer such as :py:obj:`"inner2"`. :py:obj:`use` can be one of :py:obj:`"silk", :py:obj:`mask`,
:py:obj:`paste` or :py:obj:`copper`. For internal layers, only :py:obj:`copper` is valid.
:py:obj:`side` can be one of :py:obj:`"top"`, :py:obj:`"bottom"`, :py:obj:`"mechanical"`, or a
numbered internal layer such as :py:obj:`"inner2"`. :py:obj:`use` can be one of
:py:obj:`"silk", :py:obj:`mask`, :py:obj:`paste` or :py:obj:`copper`. For internal layers,
only :py:obj:`copper` is valid.
:ivar board_name: Name of this board as parse from the input filenames, as a :py:obj:`str`. You can overwrite this
attribute with a different name, which will then be used during saving with the built-in file
naming rules.
@ -664,10 +665,10 @@ class LayerStack:
color scheme. When given, must be a dict mapping semantic :py:obj:`"side use"` layer names such
as :py:obj:`"top copper"` to a HTML-like hex color code such as :py:obj:`#ff00ea`. Transparency
is supported through 8-digit color codes. When 8 digits are given, the last two digits are used
as the layer's alpha channel. Valid side values in the layer name strings are :py:obj:`"top"` and
:py:obj:`"bottom"` as well as :py:obj:`"inner1"`, :py:obj:`"inner2"` etc. for internal layers.
Valid use values are :py:obj:`"mask"`, :py:obj:`"silk"`, :py:obj:`"paste"`, and
:py:obj:`"copper"`. For internal layers, only :py:obj:`"copper"` is valid.
as the layer's alpha channel. Valid side values in the layer name strings are :py:obj:`"top"`,
:py:obj:`"bottom"`, and :py:obj:`"mechanical"` as well as :py:obj:`"inner1"`, :py:obj:`"inner2"`
etc. for internal layers. Valid use values are :py:obj:`"mask"`, :py:obj:`"silk"`,
:py:obj:`"paste"`, and :py:obj:`"copper"`. For internal layers, only :py:obj:`"copper"` is valid.
:rtype: :py:obj:`str`
"""
if colors is None:
@ -798,6 +799,9 @@ class LayerStack:
layer.scale(factor)
def merge_drill_layers(self):
""" Merge all drill layers of this board into a single drill layer containing all objetcs. You can access this
drill layer under the :py:attr:`.LayerStack.drill_unknown` attribute. The original layers are removed from the
board. """
target = ExcellonFile(comments=['Drill files merged by gerbonara'])
for layer in self.drill_layers:
@ -810,6 +814,9 @@ class LayerStack:
self.drill_unknown = target
def normalize_drill_layers(self):
""" Take everything from all drill layers of this board, and sort it into three new drill layers: One with all
non-plated objects, one with all plated objects, and one for all leftover objects with unknown plating. This
method replaces the board's drill layers with these three sorted ones. """
# TODO: maybe also separate into drill and route?
drill_pth, drill_npth, drill_aux = [], [], []
@ -848,6 +855,8 @@ class LayerStack:
@property
def drill_layers(self):
""" Return all of this board's drill layers as a list. Returns an empty list if the board does not have any
drill layers. """
if self._drill_layers:
return self._drill_layers
if self.drill_pth or self.drill_npth or self.drill_unknown:
@ -890,6 +899,8 @@ class LayerStack:
@property
def copper_layers(self):
""" Return all copper layers of this board as a list. Returns an empty list if the board does not have any
copper layers. """
copper_layers = [ ((side, use), layer) for (side, use), layer in self.graphic_layers.items() if use == 'copper' ]
def sort_layername(val):
@ -905,17 +916,27 @@ class LayerStack:
@property
def top_side(self):
""" Return a dict containing the subset of layers from :py:meth:`~.layers.LayerStack.graphic_layers` that are on
the board's top side. Includes the board outline layer, if available. """
return { key: self[key] for key in ('top copper', 'top mask', 'top silk', 'top paste', 'mechanical outline') }
@property
def bottom_side(self):
""" Return a dict containing the subset of layers from :py:meth:`~.layers.LayerStack.graphic_layers` that are on
the board's bottom side. Includes the board outline layer, if available. """
return { key: self[key] for key in ('bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'mechanical outline') }
@property
def outline(self):
""" Return this board's outline layer if available, or :py:obj:`None`. """
return self.get('mechanical outline')
def outline_svg_d(self, tol=0.01, unit=MM):
""" Return this board's outline as SVG path data.
:param tol: :py:obj:`float` setting the tolerance below which two points are considered equal
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). SVG document unit. Default: mm
"""
chains = self.outline_polygons(tol, unit)
polys = []
for chain in chains:
@ -926,6 +947,19 @@ class LayerStack:
return ' '.join(polys)
def outline_polygons(self, tol=0.01, unit=MM):
""" Iterator yielding this boards outline as a list of ordered :py:class:`~.graphic_objects.Arc` and
:py:class:`~.graphic_objects.Line` objects. This method first sorts all lines and arcs on the outline layer into
connected components, then orders them such that one object's end point is the next object's start point,
flipping them where necessary. It yields one list of (likely mixed) :py:class:`~.graphic_objects.Arc` and
:py:class:`~.graphic_objects.Line` objects per connected component.
This method exists because the only convention in Gerber or Excellon outline files is that the outline segments
are *visually contiguous*, but that does not necessarily mean that they will be in any particular order inside
the G-code.
:param tol: :py:obj:`float` setting the tolerance below which two points are considered equal
:param unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). SVG document unit. Default: mm
"""
polygons = []
lines = [ obj.as_primitive(unit) for obj in self.outline.instance.objects if isinstance(obj, (go.Line, go.Arc)) ]
@ -975,7 +1009,7 @@ class LayerStack:
yield l
def _merge_layer(self, target, source):
def _merge_layer(self, target, source, mode='above'):
if source is None:
return
@ -983,9 +1017,18 @@ class LayerStack:
self[target] = source
else:
self[target].merge(source)
self[target].merge(source, mode)
def merge(self, other):
def merge(self, other, mode='above'):
""" Merge ``other`` into ``self``, i.e. for all layers, add all objects that are in ``other`` to ``self``. This
resets :py:attr:`.import_settings` and :py:attr:`~.CamFile.generator` on all layers. Units and other
file-specific settings are handled automatically. For the meaning of the ``mode`` parameter, see
:py:meth:`.GerberFile.merge`.
Layers are matched by their logical side and function as they are found in
:py:meth:`.LayerStack.graphic_layers`. Drill layers are normalized before merging, which splits them into
exactly three drill layers: An non-plated one, a plated one, and a (hopefully empty) unknown plating one.
"""
all_keys = set(self.graphic_layers.keys()) | set(other.graphic_layers.keys())
exclude = { tuple(key.split()) for key in STANDARD_LAYERS }
all_keys = { key for key in all_keys if key not in exclude }
@ -995,7 +1038,7 @@ class LayerStack:
for side in 'top', 'bottom':
for use in 'copper', 'mask', 'silk', 'paste':
if (side, use) in other:
self._merge_layer((side, use), other[side, use])
self._merge_layer((side, use), other[side, use], mode)
our_inner, their_inner = self.copper_layers[1:-1], other.copper_layers[1:-1]

Wyświetl plik

@ -46,6 +46,20 @@ def points_close(a, b):
class GerberFile(CamFile):
""" A single gerber file.
:ivar objects: List of objects in this Gerber file. All elements must be subclasses of :py:class:`.GraphicObject`.
:ivar comments: List of string with textual comments in the source Gerber file. These are not saved by default, but
when you call :py:meth:`.GerberFile.save` with ``drop_comments=False``, the contents of this list
will be included as comments at the top of the output file.
:ivar generator_hints: List of strings indicating which EDA tool generated this file. Hints are added to this list
during file parsing whenever the parser encounters an idiosyncratic file format variation.
:ivar import_settings: File format settings used in the original file. This can be empty if this
:py:class:`.GerberFile` was generated programatically.
:ivar layer_hints: Similar to ``generator_hints``, this is a list containing hints which layer type this file could
belong to. Usually, this will be empty, but some EDA tools automatically include layer
information inside tool-specific comments in the Gerber files they generate.
:ivar apertures: List of apertures used in this file. Make sure you keep this in sync when adding new objects.
:ivar file_attrs: List of strings with Gerber X3 file attributes. Each list item corresponds to one file attribute.
"""
def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None,
@ -83,6 +97,16 @@ class GerberFile(CamFile):
return
def merge(self, other, mode='above', keep_settings=False):
""" Merge ``other`` into ``self``, i.e. add all objects that are in ``other`` to ``self``. This resets
:py:attr:`.import_settings` and :py:attr:`~.GerberFile.generator`. Units and other file-specific settings are
handled automatically.
:param mode: One of the strings :py:obj:`"above"` (default) or :py:obj:`"below"`, specifying whether the other
layer's objects will be placed above this layer's objects (placing them towards the end of the file), or
below this layer's objects (placing them towards the beginning of the file). This setting is only relevant
when there are overlapping objects of different polarity, otherwise the rendered result will be the same
either way.
"""
if other is None:
return
@ -109,6 +133,7 @@ class GerberFile(CamFile):
self.objects += other.objects
else:
raise ValueError(f'Invalid mode "{mode}", must be one of "above" or "below".')
for obj in self.objects:
# If object has an aperture attribute, replace that aperture.
if (ap := replace_apertures.get(id(getattr(obj, 'aperture', None)))):
@ -316,7 +341,8 @@ class GerberFile(CamFile):
class GraphicsState:
""" Internal class used to track Gerber processing state during import and export. """
""" Internal class used to track Gerber processing state during import and export.
"""
def __init__(self, warn, file_settings=None, aperture_map=None):
self.image_polarity = 'positive' # IP image polarity; deprecated
@ -527,7 +553,8 @@ class GraphicsState:
class GerberParser:
""" Internal class that contains all of the actual Gerber parsing magic. """
""" Internal class that contains all of the actual Gerber parsing magic.
"""
NUMBER = r"[\+-]?\d+"
DECIMAL = r"[\+-]?\d+([.]?\d+)?"