summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/cli.rst237
-rw-r--r--gerbonara/cam.py2
-rw-r--r--gerbonara/layers.py65
-rw-r--r--gerbonara/rs274x.py31
4 files changed, 321 insertions, 14 deletions
diff --git a/docs/cli.rst b/docs/cli.rst
index e64d11c..8eb1ff3 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -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)
diff --git a/gerbonara/cam.py b/gerbonara/cam.py
index 59c4969..9819087 100644
--- a/gerbonara/cam.py
+++ b/gerbonara/cam.py
@@ -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()
diff --git a/gerbonara/layers.py b/gerbonara/layers.py
index 34d3f0c..c298ffc 100644
--- a/gerbonara/layers.py
+++ b/gerbonara/layers.py
@@ -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]
diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py
index 7bfd524..ce8b12a 100644
--- a/gerbonara/rs274x.py
+++ b/gerbonara/rs274x.py
@@ -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+)?"