diff options
-rw-r--r-- | docs/cli.rst | 237 | ||||
-rw-r--r-- | gerbonara/cam.py | 2 | ||||
-rw-r--r-- | gerbonara/layers.py | 65 | ||||
-rw-r--r-- | gerbonara/rs274x.py | 31 |
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+)?" |