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.
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.
"""Example for introspection with selective inheritance from Parser2HooksBase and Parser2Hooks."""from__future__importannotationsfrompygerber.gerberx3.parser2.context2importParser2Context,Parser2ContextOptionsfrompygerber.gerberx3.parser2.parser2importParser2,Parser2Optionsfrompygerber.gerberx3.parser2.parser2hooksimportParser2Hooksfrompygerber.gerberx3.tokenizer.tokenizerimportTokenizerfrompygerber.gerberx3.tokenizer.tokens.g04_commentimportCommentclassCustomHooks(Parser2Hooks):def__init__(self)->None:super().__init__()self.comments:list[str]=[]classCommentTokenHooks(Parser2Hooks.CommentTokenHooks):hooks:CustomHooksdefon_parser_visit_token(self,token:Comment,context:Parser2Context,)->None:self.hooks.comments.append(token.content)returnsuper().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 *"""defmain()->None:tokenizer=Tokenizer()ast=tokenizer.tokenize(GERBER_SOURCE)hooks=CustomHooks()parser=Parser2(Parser2Options(context_options=Parser2ContextOptions(hooks=hooks)),)parser.parse(ast)forcommentinhooks.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.
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.
"""Example for introspection with selective inheritance from Parser2HooksBase and Parser2Hooks."""from__future__importannotationsfrompygerber.gerberx3.parser2.attributes2importApertureAttributesfrompygerber.gerberx3.parser2.context2importParser2Context,Parser2ContextOptionsfrompygerber.gerberx3.parser2.parser2import(Parser2,Parser2OnErrorAction,Parser2Options,)frompygerber.gerberx3.parser2.parser2hooksimportParser2Hooksfrompygerber.gerberx3.parser2.parser2hooks_baseimportDefineAnyT,Parser2HooksBasefrompygerber.gerberx3.tokenizer.aperture_idimportApertureIDfrompygerber.gerberx3.tokenizer.tokenizerimportTokenizerclassCustomHooks(Parser2HooksBase):def__init__(self)->None:super().__init__()self.aperture_attributes:dict[ApertureID,ApertureAttributes]={}classApertureAttributeHooks(Parser2Hooks.ApertureAttributeHooks):passclassFileAttributeHooks(Parser2Hooks.FileAttributeHooks):passclassObjectAttributeHooks(Parser2Hooks.ObjectAttributeHooks):passclassDeleteAttributeHooks(Parser2Hooks.DeleteAttributeHooks):passclassDefineApertureTokenHooks(Parser2HooksBase.DefineApertureTokenHooks):hooks:CustomHooksdefon_parser_visit_token(self,token:DefineAnyT,context:Parser2Context,)->None:self.hooks.aperture_attributes[token.aperture_id]=(context.aperture_attributes)returnsuper().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*"""defmain()->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)foraperture,attributesinhooks.aperture_attributes.items():print(aperture)print(attributes)if__name__=="__main__":main()
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.
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.
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.
"""Example for introspection with selective inheritance from Parser2HooksBase and Parser2Hooks."""from__future__importannotationsfrompygerber.gerberx3.parser2.context2importParser2Context,Parser2ContextOptionsfrompygerber.gerberx3.parser2.errors2importParser2Error,UnitNotSet2Errorfrompygerber.gerberx3.parser2.parser2import(Parser2,Parser2OnErrorAction,Parser2Options,)frompygerber.gerberx3.parser2.parser2hooksimportParser2Hooksfrompygerber.gerberx3.state_enumsimportUnitfrompygerber.gerberx3.tokenizer.tokenizerimportTokenizerclassCustomHooks(Parser2Hooks):defon_parser_error(self,context:Parser2Context,error:Parser2Error)->None:ifisinstance(error,UnitNotSet2Error):context.set_draw_units(Unit.Inches)returnsuper().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*"""defmain()->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()