Gerber engine and viewer
How the Electronics Designer writes and reads Gerber, Excellon and job files, and compares and checks fab sets.
How SoftForge Electronics Designer writes and reads fabrication files without kicad-cli. Design note: docs/gerber-engine.md. This page covers the exporter, the reader and viewer, and compare and checks (milestone W, 1.5.0).
Exporter#
The native engine (src/gerber/) plots a .kicad_pcb straight from the board model. Its output follows
kicad-cli 10’s files line for line: the same aperture codes, aperture functions, object attributes, item order and
coordinates, so a fab house sees the same data whichever engine produced it. Only the generator lines differ: the
files name SoftForge Electronics Designer and its version (%TF.GenerationSoftware%, G04 Created by, the drill
header and the job’s Header).
Files and formats#
| File | Format | Contents |
|---|---|---|
<board>-<Layer>.gbr | RS-274X with X2 (and X3 component data) | one per plotted layer |
<board>-PTH.drl, <board>-NPTH.drl | Excellon | plated and non-plated holes, separate files |
<board>.drl | Excellon | merged holes (MixedPlating) |
<board>-front-in1.drl, <board>-in1-in2.drl, … | Excellon | blind/buried vias, one file per layer pair |
<board>-PTH-drl.gbr … | Gerber X2 | the drill files in Gerber format (Plated / NonPlated) |
<board>-PTH-drl_map.gbr … | Gerber X2 | drill map: outline, a marker per tool and a legend |
<board>-job.gbrjob | Gerber job (JSON) | general specs, design rules, file list, material stack-up |
Naming. A layer file is <board>-<layer display name> with dots replaced by underscores (blinky-F_Cu.gbr,
or the user name of a renamed layer). With Protel extensions on, the extension tells the layer: .gtl/.gbl
copper, .g1, .g2, … inner copper, .gts/.gbs mask, .gtp/.gbp paste, .gto/.gbo silk, .gta/.gba
adhesive, .gm1 Edge.Cuts, .gbr for the rest.
Gerber content.
- Header: file attributes (
.GenerationSoftware,.CreationDate,.ProjectIdwith the project GUID and revision,.SameCoordinates,.FileFunction,.FilePolarity), format 4.6 (or 4.5) in millimetres, then the aperture list. - Apertures: circles, rectangles, obrounds, polygons and KiCad’s macros (
RoundRect,Outline4P–Outline8Pfor chamfered, trapezoid and rotated pads,FreePoly<n>for custom pads,HorizOval,RotRect), each tagged with.AperFunction(SMDPad,CuDef,ComponentPad,ViaPad,Conductor,NonConductor,Profile, …). - Objects: tracks and arcs as draws, pads and vias as flashes, zones and filled shapes as regions (holes joined by
cut-ins), text from the stroke font as draws. Netlist attributes
.N(net),.P(reference and pad) and.C(component) are written only when they change, as KiCad does. - Mask files are negative-polarity openings: pads with their mask margin and vias that are not tented (per via, then the board’s tenting setting, then the dialog option).
- Paste files hold SMD pads shrunk or grown by their paste margin; DNP footprints are left out.
- The file ends with
M02, preceded by%TF.MD5%over its body when the request asks for it.
Excellon content. M48 header with the format comment and the file function, the tool table (T1C0.300 …
with TA.AperFunction comments), then the holes per tool; oval holes come after the round ones, either as G85
slots or routed (G00 … M15 G01 … M16), each followed by G05.
Job file. GeneralSpecs (project name, GUID, revision, board size from Edge.Cuts, copper layer count, thickness,
finish, impedance control), DesignRules for outer and inner layers (smallest net class clearance, narrowest track,
zone clearances), FilesAttributes for every plotted Gerber except courtyards and Margin, and MaterialStackup from
the board’s stack-up (or KiCad’s default stack-up for the thickness).
Options#
The Plot dialog’s options (src/shared/fabOptions.ts) drive both engines.
| Option | Effect in the native engine |
|---|---|
| Layers | which layer files are written, in the dialog’s order |
| Protel extensions | .gtl/.gbl/… names instead of .gbr |
| X2 attributes | off: every attribute becomes a G04 #@! … comment (KiCad’s --no-x2); no netlist attributes |
| Netlist attributes | off: no .N/.P/.C object attributes |
| Precision | coordinate format 4.6 (default) or 4.5 |
| Disable aperture macros | outline and free-form pads become regions instead of macro flashes |
| Subtract mask from silk | silk is clipped by the mask openings (%LPC% passes) |
| Tent vias | vias without their own tenting setting get no mask opening |
| Include border and title | the drawing sheet frame and title block on every layer |
| Use drill/place origin | coordinates relative to the aux origin |
| DNP | hide, sketch or cross out DNP footprints on Fab layers; the variant’s DNP overrides apply |
| Sketch pads on Fab | pad outlines instead of filled pads on Fab layers |
| Mirror, negative, drill marks | PDF/SVG/DXF only (drill marks small or actual size) |
| Drill: format | Excellon or Gerber X2 |
| Drill: units / zeros | METRIC (3:3) or INCH (2:4); decimal, suppress leading (TZ), suppress trailing (LZ) or keep zeros |
| Drill: oval holes | G85 slot (alternate) or routed |
| Drill: merge PTH and NPTH | one <board>.drl with MixedPlating |
| Drill: minimal header | no comments and no FMAT line |
| Drill: mirror Y | Y as on the board instead of negated |
| Drill: map | a Gerber drill map next to each drill file |
Panels: when the request names a panel (a .kicad_panel path or its JSON), plotBoard flattens it through the
panel generator (board copies, frame, rails and the panel’s other features) and plots the result as one board.
stepAndRepeat(image, cols, rows, pitchX, pitchY) wraps a single board’s image in a %SR% block for callers that
want a step-and-repeat file instead.
API#
import { plotBoard, writePlotSet, writeGerber, writeExcellon, writeJob } from './src/gerber';
const result = plotBoard({ boardText, boardPath, projectText, options });
// result.layers[i] = { layer, fileName, image, text }; result.drills, result.maps, result.job
await writePlotSet({ boardText, boardPath, options }, outDir); // writes every file, returns the paths
preparePlot builds the plot board once; plotLayer(pb, layer) returns one layer’s GerberImage, writeGerber
turns an image into text. drillFiles(pb) + writeExcellon and jobFromBoard + writeJob do the same for drills
and the job. src/gerber/plot.ts, writer.ts, excellon.ts and job.ts are pure TypeScript (no vscode, DOM, node or
kicad-cli); index.ts adds the panel flattening and writePlotSet, which use node’s file system.
Text: KiCad’s own glyphs#
Board text is drawn with KiCad’s stroke font when it is available, so silk and fab text are the same strokes as KiCad’s, to the nanometre:
- Glyph harvest. The first time it is needed,
src/gerber/kicadGlyphCache.tsplots a glyph table board with the user’s kicad-cli (pcb export gerbers, F.SilkS). The texts are 2.1 mm tall, so one font unit is 0.1 mm and the coordinates come back exact.src/gerber/kicadGlyphs.tschains the strokes back into glyphs. - Cache. The table is cached as
kicad-glyphs-<kicad version>-v1.json, in the extension’s global storage (VS Code) or the system temp folder (CLI and tests). It is never in the repository, the bundle or the VSIX. Without kicad-cli, text uses SoftForge Stroke. - Layout. Text is laid out in integer nanometres as KiCad does it (
src/gerber/textLayout.ts):- the pen offset is thickness / 1.52;
- the advance is rounded per glyph;
- points are truncated after the transform;
- rotations by quarter turns are exact;
- sub- and superscript offsets and the overbar match KiCad.
- Choosing the font.
PlotRequest.glyphstakes a glyph source,null(SoftForge Stroke) or nothing (the engine default).edsnx gerber export --glyphs kicad|softforgeselects it on the command line.
Matching kicad-cli#
The output is checked against kicad-cli for parity:
- Arcs. Arc centres snap to KiCad’s 100 nm / 10 nm grid (
src/gerber/arcCenter.ts). Coordinates in format 4.5 are rounded as KiCad rounds them, and I/J come from the unrounded offsets. - Drill map. The drill map (
-drl_map.gbr) is drawn as KiCad draws it:- the board edges with their own widths;
- KiCad’s 58 marker shapes, with circles from the 59th tool on, and a pen of max(0.1, d/10) mm;
- slots as sketched ovals;
- the legend in 2 mm text with KiCad’s row spacing and strings (
0.800mm / 0.0315" (12 holes),(not plated)).
- Legacy zone fills. Fills from files saved before KiCad 9 (
filled_areas_thickness, or no token with a board version below 20250210) are re-grown on load by half the zone’s min thickness with round corners, as KiCad does (src/gerber/legacyFill.ts). The plot warns “N legacy zone fill(s) … re-grown”. - Aperture macros disabled. Rounded rectangles become regions with four lines and four
G02arcs, starting where KiCad starts.
What remains after these: on pic_programmer, footprint arcs can still sit 1 nm apart. With macros disabled, zone and copper objects can come in another order; the XOR stays within the parity tolerance.
Reader and viewer#
The reader (src/gerber/parser/, W-b) parses fab files from any CAD tool into the same types the exporter writes
(src/gerber/types.ts). It never throws on content. Unknown commands, legacy constructs and guesses become lines in
warnings, and the rest of the file is still read.
import { parseGerber, parseExcellon, parseJob, parseApertureList, parseFabFile } from './src/gerber/parser';
import { flattenImage, flattenDrill, walkImage, objectPolys, geometryContext } from './src/gerber/flatten';
import { loadFabSet, readFabPath } from './src/gerber/set';
import { gerberDocument, apertureInfo, highlightIds, gerberStatus } from './src/gerber/toDocument';
const image = parseGerber(text); // GerberImage: units, format, attrs, apertures, macros, ops, warnings
const flat = flattenImage(image); // GerberFlat: polygons (mm, board frame), bbox, byAperture, byNet
const set = loadFabSet(readFabPath('fab.zip')); // { set: GerberSet (layer views), files: parsed files }
Coordinates and units#
GerberImage.ops and DrillFile.ops keep the file’s units (units) and orientation (y up). They are exactly what the
file says, so writeGerber(parseGerber(t)) round-trips. flattenImage / flattenDrill convert to mm. With
frame: 'board' (the default) they map (x, y) to (x, −y): this is the KiCad board frame for KiCad plots (no aux
origin) and the renderer’s frame. frame: 'gerber' keeps y up.
What the reader covers#
| Construct | Handling |
|---|---|
%FS…% | L/T zero omission, A/I absolute/incremental, any digit counts; decimal points |
%MO…%, G70/G71 | mm / inch |
%AD…% | C/R/O/P with holes, macro instances (ADD10RoundRect,0.25X…) |
%AM…% | $n variables and $n= assignments, + - x /, parentheses, unary minus; primitives 1, 4, 5, 6, 7, 20, 21 (2 and 22 converted); 0 comments kept in source |
| D01/D02/D03, Dnn, G54Dnn | modal D-codes (coordinates without a D-code reuse the last one, with a warning) |
| G01/G02/G03, G74/G75 | multi-quadrant arcs; single-quadrant arcs pick the centre of the four ±I/±J candidates that is equidistant from both ends with a sweep ≤ 90° |
| G36/G37 | several contours per region, arcs discretised (1 µm chord error) |
%LP%, %LM%, %LR%, %LS% | emitted as ops; the flattener applies polarity in order and the transforms to the aperture images |
%SR% | stepRepeat ops; expanded when walking / flattening |
%TF%, %TA%, %TO%, %TD%, G04 #@! … | file attributes, aperture attributes (.AperFunction), object attributes (.N, .P, .C) on each flash/draw/arc/region |
X3 (.C, .CRot, .CMfr, .CMPN, .CVal, .CFtp, .CPgN, .CHgt, .CLbN, .CLbD, .CSup, .CMnt) | a component op at the component’s flash |
| IP/OF/MI/SF/IR/AS/IN/LN | deprecated: read, warned, ignored (IPNEG marks the image Negative) |
| RS-274D | no % commands: apertures from an aperture list (parseApertureList: D10 ROUND 10 mil, D12 RECT 60X30 …), else a thin default circle per D-code |
Excellon: the M48 header (METRIC/INCH, ,LZ / ,TZ, 000.000 digit formats, FMAT, ICI), tools with F/S parameters,
Tn changes, hits, G85 slots, routed G00 … M15 G01/G02/G03 … M16 paths (two points make a slot), R repeats,
G90/G91, KiCad’s ;TYPE=PLATED|NON_PLATED and ; #@! TF/TA comments, ; FORMAT={…} and Altium’s
;FILE_FORMAT=i:d. Files without a header get their units from the tool sizes and their zero suppression from the
coordinate strings, with a warning for each guess.
Fab sets#
loadFabSet classifies files by extension and content and parses them. It names each layer from the X2
.FileFunction, the job file’s FilesAttributes, or the file name (KiCad -F_Cu.gbr, -In1_Cu.gbr,
-Edge_Cuts.gbr; Protel .gtl .gbl .gts .gbs .gto .gbo .gtp .gbp .gm1 .gko .g1 .g2…; .drl .xln .txt drills). It also
picks colours per function and the default stacking:
- copper (top to bottom)
- masks (45 % opacity)
- legends
- pastes (50 % opacity)
- profile
- drawings
- drills
readFabFolder, readFabZip (a minimal zip reader on node’s zlib, no dependency) and readFabPath load the files.
Viewer document#
gerberDocument(loaded, state, title) turns a set into the renderer’s Document, with doc.gerber holding the
set, the aperture tables, warnings and the status line:
- C/R/O flashes become
padprimitives. - Round-aperture draws become
lineandarcprimitives. - Macros, polygons, apertures with holes, rectangle draws and regions become
polygonprimitives.
Every primitive carries itemId = g:<layer>:<op>[.<sub>]:<D-code|R|H-tool> and its X2 net. Picking, the net
highlight and apertureInfo / highlightIds all work from that. Layers with clear polarity, and layers shown
negative, are drawn from the flattened image instead. Those are listed in doc.gerber.flattened.
A click on a flattened layer is picked by geometry on the host (src/gerber/pick.ts pickObject, used by
apertureInfo):
- Flashes by the aperture shape: circle, rectangle and obround exactly, with their hole and with LM / LR / LS applied; polygons and macros by their outline.
- Draws as a stroke of the aperture width.
- Arcs as an annulus sector with round caps.
- Regions by their polygon.
The image is walked in file order. A later clear object removes what it covers, and a later dark object covers it again. The click tolerance (three screen pixels) widens dark objects only. In 3D mode the layers take their KiCad names and drills become holes, so the board’s 3D view stacks them with the job file’s thicknesses.
Commands#
| Command | Result |
|---|---|
edsnx gerber parse <file> --json | one file: units, format, function and name, X2 attributes, apertures (size mm, function, uses), macro names, object counts, bbox and area (mm, board frame), parse time, warnings |
edsnx gerber set <folder|zip|file> --json | the set: layers (id, file, name, function, kind, side, colour, opacity, order), job (project, size, layers, thickness, stack-up), status line, per-file warnings |
softforge.edsnx.gerber.openSet | VS Code: open a folder or zip in the viewer |
Files changed on disk#
The views follow edits made on disk (src/gerber/sidecarWatch.ts):
- Gerber views. Each open Gerber view watches its set, a folder or zip, also outside the workspace. When a Gerber, drill, job or zip file changes, the view re-reads the set.
- Documentation sidecars. A workspace watcher sends hand edits of a
*.kicad_doc.jsonsidecar toDocHost.onSidecarChanged. The open sheet then shows the edited views and tables as stale, including the views drawn from the Gerber plot. - Review. A compare review whose set changed shows a stale banner with Re-run.
Compare and checks#
Compare and the Gerber checks (W-c) work on the flattened polygons (src/gerber/compare.ts, checks.ts), so they
read files from any CAD tool. Boolean operations run in Clipper’s loose mode: strictly simple output is far too slow
on large zone fills and changes nothing for areas and distances.
Compare#
compareSets(a, b, { tolerancePct = 0.01, shapes, ignore, maxRegions }) pairs the layers of two sets by X2
.FileFunction (file names otherwise; Copper,L1,Top and Copper,L1,Top,Signal pair, PTH and NPTH pair with
their drill files) and returns a LayerDiff per pair:
- the dark area of each side and the XOR area (a − b and b − a), with its share of the layer;
- the XOR regions, clustered within 0.05 mm, each with its bbox, area and side (only in a / only in b), largest first;
- aperture function counts (
flash ComponentPad,draw Conductor,region …) and, withshapes, flash shapes; - drill hits per diameter and plating;
- file attribute differences (
FileFunction,FilePolarity,Part,SameCoordinates,ProjectId).
A pair matches when its XOR share is within the tolerance and nothing else differs; the set matches when every pair
does and no layer is left unmatched. Either side may be a .kicad_pcb: it is plotted in memory by the native engine
with the other set’s options (layers, precision, macros, mask subtraction), which answers “did the plot match the
board?”.
Checks#
runGerberChecks(set, { settings, rules }) returns findings (GerberFinding: rule, severity, layer, location in mm,
board frame, measured value and limit) and a summary per rule.
| Rule | Severity | What it measures |
|---|---|---|
gbr_min_feature | error | copper narrower than the limit (morphological opening; residues under 0.01 mm ignored) |
gbr_min_clearance | error | gap between copper islands of different nets (X2 .N; any two islands without a netlist) |
gbr_min_annular | error | copper ring around plated holes on the outer layers |
gbr_sliver | warning | solder mask webs narrower than the limit (closing of the openings) |
gbr_mask_opening | warning | pads (pad aperture functions) without a mask opening |
gbr_paste_opening | warning | paste openings more than 5 % off copper |
gbr_outline_open | error | Profile segments that do not close into loops |
gbr_drill_to_copper | error | holes closer than the limit to copper of another net |
gbr_missing_layer | error | a job file entry or a usual layer (copper, mask, outline, drill) without a file |
gbr_attribute_mismatch | warning | files whose X2 function or polarity differ from the job file |
Limits default to 0.1 mm (feature, clearance, annular, sliver) and 0.2 mm (drill to copper). settingsFromProfile
maps a DFM profile: track width → min feature, clearance, annular ring, mask sliver, hole to copper.
findingsAsViolations turns findings into DRC-style violations (the rule ids above).
Rendering#
src/gerber/preview.ts renders a set without a browser: renderSetRgba (a scanline rasteriser with the viewer’s
colours, opacities and stacking; encodePng writes it), renderSetSvg and renderSetPdf (vector PDF 1.4 at 1:1 with
transparent masks). plotNativeSet(board) plots a board in memory and reads it back; compareBoardToSet and
plotLayerPolys build on it.
Viewer, Plot dialog and documentation#
The viewer’s right Gerber tab has Compare (board vs Gerber, folder vs folder) and Checks sections and a PDF export button; see the user guide, Comparing and checking Gerbers.
Open in review shows the review view over the canvas (
src/gerber/review.tsbuildReview):- one row per layer: its XOR thumbnail (grey for both, orange for only in a, magenta for only in b), both areas, the XOR %, the regions (click one to centre the canvas there), aperture and attribute differences, and a status pill;
- layers found in one set only;
- Export Markdown writes the same review as Markdown (
reviewMarkdown), with the XOR images, both sets on one frame and the visual diff; - a stale banner with Re-run when a file of either set changes on disk.
The command Electronics Designer: Review Gerber vs Gerber (
softforge.edsnx.gerber.compareReview) opens the review of the active viewer’s last compare, or asks for two fab sets.The Plot dialog’s Engine (kicad-cli | native), Preview tab (the written files read back by the reader) and Open in Gerber viewer button; see Fabrication outputs.
Documentation fabrication views take
source: 'plot'(Copper from: plot in the view options): copper and mask come from the native plot’s polygons, so the drawing equals the files.Output jobs: the
gerberstep takesengine: 'native'(runsgerber.export) or'kicad-cli'; thegerberCheckstep (off by default in the fab job) runsgerber.checkon the step’s folder withfailOn: 'error'and writes a JSON report.
Commands#
| Command | Result |
|---|---|
edsnx gerber export board.kicad_pcb --out fab/ [--engine native|kicad-cli] | plot Gerbers, drills, maps and the job file; flags mirror the Plot dialog |
edsnx gerber view fab/ --png fab.png [--svg …] [--pdf …] [--layers B.Cu,B.Mask] [--flipped] | render a set or a board |
edsnx gerber compare board.kicad_pcb fab/ --json | board vs set, or set vs set (--tolerance, --shapes, --png diff.png) |
edsnx gerber check fab.zip --profile jlcpcb-4 --json | Gerber DFM (--rules, single limits, --fail-on error, --out report.json); --load also opens a folder set with gerbv or tracespace (load in the result, skipped with the reason when neither is installed) |
edsnx gerber job fab/ --json | the job file with missing files and mismatches; with a board, the native job file |
Parity#
npm run gerber:parity (scripts/gerber-parity.mjs, --board, --json, --diffs) plots each fixture with kicad-cli
and the native engine and compares the sets; test/gerber.parity.test.ts asserts the same (skipped without kicad-cli).
Set KICAD_DEMOS to add KiCad’s demo projects. With KiCad’s glyphs, silk is byte-identical to kicad-cli
(KNOWN_GAPS is empty), and the bodies of the drill maps match. With macros disabled, mask and paste are
byte-identical.
The test also loads the native set with gerbv (gerbv --export=png), or with tracespace when it is installed, the way
a fab house would (src/gerber/loadCheck.ts fabLoadCheck). When neither is installed, the check reports skipped:
with the reason and the test is skipped with it. It never passes silently.