Architecture¶
Overview¶
gerberdiff is a diff tool for Gerber and Excellon PCB design files. It turns the question "what changed between two board revisions?" into visual overlays and machine-readable reports via two complementary engines that share the same parse layer:
- the raster engine (
render/+diff/) -- Cairo rasterisation + pixel XOR; produces visual overlay PNGs and screen-space changed regions; - the geometry engine (
geometry/) -- shapely vector geometry; produces resolution-independent, attributed changes (moved/resized/added/removed) and is Cairo-free. See geometry-diff.md.
Gerber/Excellon files
|
v
+-------------+
| parse/ | tokenise -> state machine -> ParsedImage IR
+------+------+
| ParsedImage
+------------+--------------+
| |
v v
+-------------+ +---------------+
| render/ | | geometry/ | expand ops -> shapely,
| viewport ->| | signatures ->| exact cancellation,
| Cairo -> | | boolean diff | KD-tree attribution
| numpy | +-------+-------+
+------+------+ |
| numpy BGRA | GeometryDiffResult
v |
+-------------+ |
| diff/ | XOR -> scipy CCL |
+------+------+ |
| DiffResult |
+------------+----------------+
v
+-------------+
| export/ | JSON v1/v2, overlay PNG, SVG
+-------------+
Module map¶
gerberdiff/parse/¶
| File | Purpose |
|---|---|
tokenizer.py |
Splits a Gerber file into a flat stream of Token objects (param blocks, data blocks, D/G/M codes) |
gerber_parser.py |
Utility functions: parse_format_statement, convert_coordinate, parse_aperture_definition -- called directly by gerber_state.py |
gerber_state.py |
Full RS-274X state machine; consumes the token stream from tokenize_gerber and emits DrawOp / RegionFill objects into a ParsedImage |
macro_parser.py |
Parses and evaluates aperture macro expressions; produces MacroDef objects used by the renderer |
arc_math.py |
Converts Gerber centre-offset arc representation to ArcSegment (centre + radius + start/end angles) |
excellon_parser.py |
Parses Excellon drill files (header + body) into a ParsedImage using the same IR |
gerberdiff/render/¶
| File | Purpose |
|---|---|
viewport.py |
Fits a BoundingBox into pixel canvas dimensions -> Viewport (pan/zoom + Y-flip) |
compiled_render.py |
Translates a ParsedImage IR into a flat list of DrawOp objects |
draw_ops.py |
Low-level cairocffi primitives for each draw operation (stroke, fill, flash, arc) |
macro_renderer.py |
Evaluates MacroDef primitives (circle, line, outline, polygon, thermal, moire, custom) to cairocffi paths |
renderer.py |
Orchestrates: creates cairo.ImageSurface, calls compiled render + draw-ops, returns numpy BGRA array |
gerberdiff/diff/¶
| File | Purpose |
|---|---|
diff_engine.py |
Renders both images to a shared viewport, XORs RGB channels, runs scipy CCL, returns SingleLayerDiff |
layer_matcher.py |
Pairs files from two directories by stem; classifies each by LayerType; returns sorted list[LayerPair] |
gerberdiff/geometry/¶
| File | Purpose |
|---|---|
primitives.py |
Adaptive-tessellation shapely shape builders (circle, rect, obround, n-gon, arc sampling) |
macro_geom.py |
Macro primitives -> shapely with spec-compliant exposure scoping and rotation |
expand.py |
Flash/stroke/region expansion: exact Minkowski strokes, even-odd regions |
layer_geometry.py |
Lazy ExpandedOp assembly: source-based signatures, polarity replay, transforms, S&R, block flattening |
geom_diff.py |
Boolean added/removed material with exact-cancellation fast path |
attribute.py |
Exact + KD-tree matching; classifies moved / resized / added / removed |
driver.py |
compute_geometry_diff: directory pairing (reuses layer_matcher), per-layer orchestration |
types.py |
Public result types: GeometryChange, LayerGeometryDiff, GeometryDiffResult |
gerberdiff/export/¶
| File | Purpose |
|---|---|
json_report.py |
Builds versioned JSON diff reports: v1 from a DiffResult, v2 from a GeometryDiffResult |
png_export.py |
Builds a colour-coded overlay PNG: red = removed, green = added, yellow = changed, grey = common |
svg_export.py |
Cairo-free SVG overlay for geometry diffs (red/green/blue/orange change kinds) |
gerberdiff/¶
| File | Purpose |
|---|---|
types.py |
All shared IR dataclasses and enums (see below) |
cli.py |
Click entry point; subcommands: parse, render, diff, geomdiff |
Internal representation (IR)¶
All coordinate values are in inches throughout the IR. The parse layer converts from whatever unit the file uses (inches or mm) before emitting nets.
Key types (gerberdiff/types.py)¶
ParsedImage
+-- draw_ops: list[DrawOp | RegionFill] <- one entry per drawing operation
+-- apertures: dict[int, Aperture] <- keyed by D-code number
+-- layers: list[LayerState] <- polarity, rotation, mirror, scale, S&R
+-- coord_states: list[CoordState] <- coordinate format, unit, offsets
+-- bounding_box: BoundingBox <- axis-aligned hull in inches
+-- diagnostics: list[Diagnostic]
DrawOp
+-- start_x / start_y / stop_x / stop_y (inches)
+-- aperture_index, aperture_state (Off / On / Flash)
+-- interpolation (Linear / CW / CCW)
+-- layer_index, coord_state_index
+-- arc_segment: ArcSegment | None (fully resolved, angles in degrees)
Aperture union type:
CircleAperture | RectangleAperture | ObroundAperture | PolygonAperture | MacroAperture | BlockAperture
Coordinate system and viewport¶
Gerber uses a right-handed coordinate system (+Y up). Cairo uses +Y down. The viewport transform applies a Y-flip:
where \((s_x, s_y)\) are screen (pixel) coordinates, \((w_x, w_y)\) are world (inch) coordinates, \((p_x, p_y)\) is the pan offset, and \(z\) is the zoom factor (pixels per inch).
The zoom is computed to fit the bounding box into the canvas with a 10% margin:
where \(W, H\) are the canvas dimensions in pixels and \(b_w, b_h\) are the bounding box width and height in inches. The pan is then set so the board centre maps to the canvas centre:
The inverse transform (screen_to_world) recovers world coordinates from
pixel coordinates:
compute_viewport fits the board's bounding box into the canvas with a 10%
margin. merge_bounding_boxes is used by the diff engine to derive a single
shared viewport so both images are aligned before pixel comparison.
Raster diff algorithm¶
The geometry engine's algorithm is documented separately in geometry-diff.md; it shares step 1 (layer matching).
-
Layer matching (
layer_matcher.py) -- pairs files from two directories by file stem. Files present only in one directory are recorded asstatus="added"or"removed". Results are sorted by a canonical layer order (F.Cu -> B.Cu -> inner Cu -> masks -> paste -> silk -> edge cuts -> drill -> unknown). -
Shared viewport (
diff_engine.py) -- merges the bounding boxes of both images so that both are rasterised at the same scale and position. -
XOR -- RGB channels of the two BGRA numpy arrays are XORed. A pixel is "changed" when any RGB channel differs (alpha is ignored).
-
Connected-component labelling (CCL) --
scipy.ndimage.labelwith 4-connectivity identifies contiguous regions of changed pixels. -
Region extraction --
find_objects+center_of_massproduce pixel-space bounding boxes and centroids, which are converted to inch coordinates viascreen_to_world. -
Merge --
merge_overlapping_regionsiteratively merges regions whose bounding boxes overlap within a configurable tolerance (default 0.05 in).
Extension points¶
New aperture type -- add an @dataclass to types.py, add a Literal arm to
the Aperture type alias, handle the new type in gerber_state.py (parsing) and
compiled_render.py / draw_ops.py (rendering).
New exporter -- add a module under gerberdiff/export/, accept a DiffResult
and write output; wire it into the diff subcommand in cli.py.
New file format -- add a parser module under gerberdiff/parse/ that produces
a ParsedImage; the entire render/diff pipeline works unchanged downstream.