Skip to content

Usage

Introduction

Since release 2.2.0 PyGerber offers interface designed for Gerber code introspection based on Parser2 class and visitor pattern. API is build around Parser2HooksBase class from pygerber.gerberx3.parser2.parser2hooks_base module and descendant classes passed to Parser2 class. Parser2 visits all tokens in Gerber AST created by Tokenizer and invokes particular hooks from provided hooks class. Parser2HooksBase itself doesn't implement any Gerber specific behaviors. It is just a collection of classes with empty hook methods which can be used to implement behaviors explained in The Gerber Format Specification. PyGerber provides such implementation in form of Parser2Hooks class, available in pygerber.gerberx3.parser2.parser2 module.

Minimal example

Let's consider very simple example in which we are interested in extracting all comments from Gerber code. Of course for such task it would be just enough to use regular expressions, but thanks to simplicity of this task it will be easier to perceive how hooks work.

test/examples/introspect_minimal_example.py
"""Example for introspection with selective inheritance from Parser2HooksBase and Parser2Hooks."""

from __future__ import annotations

from pygerber.gerberx3.parser2.context2 import Parser2Context, Parser2ContextOptions
from pygerber.gerberx3.parser2.parser2 import Parser2, Parser2Options
from pygerber.gerberx3.parser2.parser2hooks import Parser2Hooks
from pygerber.gerberx3.tokenizer.tokenizer import Tokenizer
from pygerber.gerberx3.tokenizer.tokens.g04_comment import Comment


class CustomHooks(Parser2Hooks):
    def __init__(self) -> None:
        super().__init__()
        self.comments: list[str] = []

    class CommentTokenHooks(Parser2Hooks.CommentTokenHooks):
        hooks: CustomHooks

        def on_parser_visit_token(
            self,
            token: Comment,
            context: Parser2Context,
        ) -> None:
            self.hooks.comments.append(token.content)
            return super().on_parser_visit_token(token, context)


GERBER_SOURCE = r"""
G04 Ucamco ex. 2: Shapes*           G04 A comment                                                            *
G04 Ucamco ex. 2: Shapes*           G04 Comment                                                              *
%MOMM*%                             G04 Units are mm                                                         *
%FSLAX36Y36*%                       G04 Format specification:                                                *
                                    G04  Leading zeros omitted                                               *
                                    G04  Absolute coordinates                                                *
                                    G04  Coordinates in 3 integer and 6 fractional digits.                   *
%TF.FileFunction,Other,Sample*%     G04 Attribute: the is not a PCB layer, it is just an                     *
                                    G04 example                                                              *
G04 Define Apertures*               G04 Comment                                                              *
%AMTHERMAL80*                       G04 Define the aperture macro 'THERMAL80'                                *
7,0,0,0.800,0.550,0.125,45*%        G04 Use thermal primitive in the macro                                   *
%ADD10C,0.1*%                       G04 Define aperture 10 as a circle with diameter 0.1 mm                  *
%ADD11C,0.6*%                       G04 Define aperture 11 as a circle with diameter 0.6 mm                  *
%ADD12R,0.6X0.6*%                   G04 Define aperture 12 as a rectangle with size 0.6 x 0.6 mm             *
%ADD13R,0.4X1.00*%                  G04 Define aperture 13 as a rectangle with size 0.4 x 1 mm               *
%ADD14R,1.00X0.4*%                  G04 Define aperture 14 as a rectangle with size 1 x 0.4 mm               *
%ADD15O,0.4X01.00*%                 G04 Define aperture 15 as an obround with size 0.4 x 1 mm                *
%ADD16P,1.00X3*%                    G04 Define aperture 16 as a polygon with 3 vertices and                  *
                                    G04 circumscribed circle with diameter 1 mm                              *
%ADD19THERMAL80*%                   G04 Define aperture 19 as an instance of macro aperture                  *
                                    G04 'THERMAL80' defined earlier                                          *
G04 Start image generation*         G04 A comment                                                            *
D10*                                G04 Select aperture 10 as current aperture                               *
X0Y2500000D02*                      G04 Set the current point to (0, 2.5) mm                                 *
G01*                                G04 Set linear plot mode                                                 *
X0Y0D01*                            G04 Create draw with the current aperture                                *
X2500000Y0D01*                      G04 Create draw with the current aperture                                *
X10000000Y10000000D02*              G04 Set the current point                                                *
X15000000D01*                       G04 Create draw with the current aperture                                *
X20000000Y15000000D01*              G04 Create draw with the current aperture                                *
X25000000D02*                       G04 Set the current point.                                               *
Y10000000D01*                       G04 Create draw with the current aperture                                *
D11*                                G04 Select aperture 11 as current aperture                               *
X10000000Y10000000D03*              G04 Create flash with the current aperture (11) at (10, 10).             *
X20000000D03*                       G04 Create a flash with the current aperture at (20, 10).                *
M02*                                G04 End of file                                                          *
"""


def main() -> None:
    tokenizer = Tokenizer()
    ast = tokenizer.tokenize(GERBER_SOURCE)
    hooks = CustomHooks()
    parser = Parser2(
        Parser2Options(context_options=Parser2ContextOptions(hooks=hooks)),
    )
    parser.parse(ast)

    for comment in hooks.comments:
        print(comment)


if __name__ == "__main__":
    main()

As you can see in snippet above, to inject custom hooks class one must create nested options structure. Design decision to nest configuration like this, was made to allow maximal customization of all parts of the Parser2. Indeed at each level there are few options useful in specific situations. But as for now, let's focus on hooks themselves.

It's important to notice that hook method on_parser_visit_token() is overrode in nested class, CommentTokenHooks, which inherits from Parser2Hooks. There are hooks which are defined directly in hooks class, but they are more general, eg. for handling exceptions. All hooks specific to particular tokens are defined in nested classes named in way indicating what token they are concerned with, eg. DefineApertureObroundTokenHooks, ImagePolarityTokenHooks.

Output of this code will look like this:

 Ucamco ex. 2: Shapes
 A comment
 Ucamco ex. 2: Shapes
 Comment
 Units are mm
 Format specification:
  Leading zeros omitted
  Absolute coordinates
  Coordinates in 3 integer and 6 fractional digits.
 Attribute: the is not a PCB layer, it is just an
 example
 Define Apertures
 Comment
 Define the aperture macro 'THERMAL80'
 Use thermal primitive in the macro
 Define aperture 10 as a circle with diameter 0.1 mm
 Define aperture 11 as a circle with diameter 0.6 mm
 Define aperture 12 as a rectangle with size 0.6 x 0.6 mm
 Define aperture 13 as a rectangle with size 0.4 x 1 mm
 Define aperture 14 as a rectangle with size 1 x 0.4 mm
 Define aperture 15 as an obround with size 0.4 x 1 mm
 Define aperture 16 as a polygon with 3 vertices and
 circumscribed circle with diameter 1 mm
 Define aperture 19 as an instance of macro aperture
 'THERMAL80' defined earlier
 Start image generation
 A comment
 Select aperture 10 as current aperture
 Set the current point to (0, 2.5) mm
 Set linear plot mode
 Create draw with the current aperture
 Create draw with the current aperture
 Set the current point
 Create draw with the current aperture
 Create draw with the current aperture
 Set the current point.
 Create draw with the current aperture
 Select aperture 11 as current aperture
 Create flash with the current aperture (11) at (10, 10).
 Create a flash with the current aperture at (20, 10).

Notice that every line starts with one space, as everything directly after G04 statement is considered a comment, including leading spaces.

Mixed inheritance

By default Parser2 is using Parser2Hooks, however, for some use cases it may be more beneficial to use Parser2HooksBase class to reduce time required to traverse single Gerber file. This is the case when one needs only selected Gerber features, eg. attribute support. In such case, you can create new Parser2HooksBase derived class and for some hook classes inherit from Parser2Hooks nested classes.

For example let's assume we want to extract attributes of all apertures in Gerber file. To do it we need a working attribute cumulation logic, but at the same time let's try to minimize time required to parse file by using only these parts of Parser2Hooks which are necessary.

test/examples/introspect_mixed_inheritance.py
"""Example for introspection with selective inheritance from Parser2HooksBase and Parser2Hooks."""

from __future__ import annotations

from pygerber.gerberx3.parser2.attributes2 import ApertureAttributes
from pygerber.gerberx3.parser2.context2 import Parser2Context, Parser2ContextOptions
from pygerber.gerberx3.parser2.parser2 import (
    Parser2,
    Parser2OnErrorAction,
    Parser2Options,
)
from pygerber.gerberx3.parser2.parser2hooks import Parser2Hooks
from pygerber.gerberx3.parser2.parser2hooks_base import DefineAnyT, Parser2HooksBase
from pygerber.gerberx3.tokenizer.aperture_id import ApertureID
from pygerber.gerberx3.tokenizer.tokenizer import Tokenizer


class CustomHooks(Parser2HooksBase):
    def __init__(self) -> None:
        super().__init__()
        self.aperture_attributes: dict[ApertureID, ApertureAttributes] = {}

    class ApertureAttributeHooks(Parser2Hooks.ApertureAttributeHooks):
        pass

    class FileAttributeHooks(Parser2Hooks.FileAttributeHooks):
        pass

    class ObjectAttributeHooks(Parser2Hooks.ObjectAttributeHooks):
        pass

    class DeleteAttributeHooks(Parser2Hooks.DeleteAttributeHooks):
        pass

    class DefineApertureTokenHooks(Parser2HooksBase.DefineApertureTokenHooks):
        hooks: CustomHooks

        def on_parser_visit_token(
            self,
            token: DefineAnyT,
            context: Parser2Context,
        ) -> None:
            self.hooks.aperture_attributes[token.aperture_id] = (
                context.aperture_attributes
            )
            return super().on_parser_visit_token(token, context)


GERBER_SOURCE = r"""
%TF.GenerationSoftware,KiCad,Pcbnew,5.1.5-52549c5~84~ubuntu18.04.1*%
%TF.CreationDate,2020-02-11T15:54:30+02:00*%
%TF.ProjectId,A64-OlinuXino_Rev_G,4136342d-4f6c-4696-9e75-58696e6f5f52,G*%
%TF.SameCoordinates,Original*%
%TF.FileFunction,Copper,L6,Bot*%
%TF.FilePolarity,Positive*%
%FSLAX46Y46*%
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
G04 Created by KiCad (PCBNEW 5.1.5-52549c5~84~ubuntu18.04.1) date 2020-02-11 15:54:30*
%MOMM*%
%LPD*%
G04 APERTURE LIST*
%TA.AperFunction,EtchedComponent*%
%ADD10C,0.508000*%
%TD*%
%TA.AperFunction,EtchedComponent*%
%ADD11C,0.254000*%
%TD*%
%TA.AperFunction,ComponentPad*%
%ADD12O,2.800000X2.000000*%
%TD*%
%TA.AperFunction,ComponentPad*%
%ADD13C,1.650000*%
M02*
"""


def main() -> None:
    tokenizer = Tokenizer()
    ast = tokenizer.tokenize(GERBER_SOURCE)
    hooks = CustomHooks()
    parser = Parser2(
        Parser2Options(
            context_options=Parser2ContextOptions(hooks=hooks),
            on_update_drawing_state_error=Parser2OnErrorAction.UseHook,
        ),
    )
    parser.parse(ast)

    for aperture, attributes in hooks.aperture_attributes.items():
        print(aperture)
        print(attributes)


if __name__ == "__main__":
    main()

Output of this code will look like this:

D10
ApertureAttributes({'.AperFunction': 'EtchedComponent'})
D11
ApertureAttributes({'.AperFunction': 'EtchedComponent'})
D12
ApertureAttributes({'.AperFunction': 'ComponentPad'})
D13
ApertureAttributes({'.AperFunction': 'ComponentPad'})

Beware that there are some potential risks when using such approach. Tokens often rely on other tokens defined before them (eg. CoordinateFormat relies on UnitMode). For example in this case we can't inherit from Parser2Hooks.DefineApertureCircleTokenHooks, as we are not including implementation of UnitModeTokenHooks, so define would complain about draw units not being set, by throwing pygerber.gerberx3.parser2.errors2.UnitNotSet2Error.

Error handling

Parser2 hooks provide a way to handle errors before they are propagated to Parser2 and cause parse interruption. However, to enable this behavior one must explicitly enable it by setting on_update_drawing_state_error parameter to Parser2OnErrorAction.UseHook.

Parser2(
    Parser2Options(
        context_options=Parser2ContextOptions(hooks=hooks),
        on_update_drawing_state_error=Parser2OnErrorAction.UseHook,
    ),
)

This option gives parser a chance to recover from error by passing it to one of two hooks: on_parser_error() on on_other_error(). First one is used to handle exceptions are not descendants of pygerber.gerberx3.parser2.errors2.Parser2Error, which are expected to be thrown by parser related code, mostly when encountering unrecoverable Gerber standard violations. They are "unrecoverable" in a sense that we can't make a good general assumption what should we do with it. pygerber.gerberx3.parser2.errors2.UnitNotSet2Error is an example of such an error, raised when attempting to interpret Gerber coordinates before unit of distance was set (inch/millimeter), which leaves units as undefined and neither inch nor millimeter is a good default in general case, but one of them can be a good default in some specific environments.

test/examples/introspect_handle_no_unit.py
"""Example for introspection with selective inheritance from Parser2HooksBase and Parser2Hooks."""

from __future__ import annotations

from pygerber.gerberx3.parser2.context2 import Parser2Context, Parser2ContextOptions
from pygerber.gerberx3.parser2.errors2 import Parser2Error, UnitNotSet2Error
from pygerber.gerberx3.parser2.parser2 import (
    Parser2,
    Parser2OnErrorAction,
    Parser2Options,
)
from pygerber.gerberx3.parser2.parser2hooks import Parser2Hooks
from pygerber.gerberx3.state_enums import Unit
from pygerber.gerberx3.tokenizer.tokenizer import Tokenizer


class CustomHooks(Parser2Hooks):
    def on_parser_error(self, context: Parser2Context, error: Parser2Error) -> None:
        if isinstance(error, UnitNotSet2Error):
            context.set_draw_units(Unit.Inches)
        return super().on_parser_error(context, error)


GERBER_SOURCE = r"""
%FSLAX46Y46*%
G04 Let's not include MO command. *
%LPD*%
G04 APERTURE LIST*
%TA.AperFunction,EtchedComponent*%
%ADD10C,0.508000*%
%TD*%
%TA.AperFunction,EtchedComponent*%
%ADD11C,0.254000*%
%TD*%
%TA.AperFunction,ComponentPad*%
%ADD12O,2.800000X2.000000*%
%TD*%
%TA.AperFunction,ComponentPad*%
%ADD13C,1.650000*%
M02*
"""


def main() -> None:
    tokenizer = Tokenizer()
    ast = tokenizer.tokenize(GERBER_SOURCE)
    hooks = CustomHooks()
    parser = Parser2(
        Parser2Options(
            context_options=Parser2ContextOptions(hooks=hooks),
            on_update_drawing_state_error=Parser2OnErrorAction.UseHook,
        ),
    )
    parser.parse(ast)


if __name__ == "__main__":
    main()