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#

FileFormatContents
<board>-<Layer>.gbrRS-274X with X2 (and X3 component data)one per plotted layer
<board>-PTH.drl, <board>-NPTH.drlExcellonplated and non-plated holes, separate files
<board>.drlExcellonmerged holes (MixedPlating)
<board>-front-in1.drl, <board>-in1-in2.drl, …Excellonblind/buried vias, one file per layer pair
<board>-PTH-drl.gbr …Gerber X2the drill files in Gerber format (Plated / NonPlated)
<board>-PTH-drl_map.gbr …Gerber X2drill map: outline, a marker per tool and a legend
<board>-job.gbrjobGerber 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, .ProjectId with 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–Outline8P for 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.

OptionEffect in the native engine
Layerswhich layer files are written, in the dialog’s order
Protel extensions.gtl/.gbl/… names instead of .gbr
X2 attributesoff: every attribute becomes a G04 #@! … comment (KiCad’s --no-x2); no netlist attributes
Netlist attributesoff: no .N/.P/.C object attributes
Precisioncoordinate format 4.6 (default) or 4.5
Disable aperture macrosoutline and free-form pads become regions instead of macro flashes
Subtract mask from silksilk is clipped by the mask openings (%LPC% passes)
Tent viasvias without their own tenting setting get no mask opening
Include border and titlethe drawing sheet frame and title block on every layer
Use drill/place origincoordinates relative to the aux origin
DNPhide, sketch or cross out DNP footprints on Fab layers; the variant’s DNP overrides apply
Sketch pads on Fabpad outlines instead of filled pads on Fab layers
Mirror, negative, drill marksPDF/SVG/DXF only (drill marks small or actual size)
Drill: formatExcellon or Gerber X2
Drill: units / zerosMETRIC (3:3) or INCH (2:4); decimal, suppress leading (TZ), suppress trailing (LZ) or keep zeros
Drill: oval holesG85 slot (alternate) or routed
Drill: merge PTH and NPTHone <board>.drl with MixedPlating
Drill: minimal headerno comments and no FMAT line
Drill: mirror YY as on the board instead of negated
Drill: mapa 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#

ts
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.ts plots 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.ts chains 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.glyphs takes a glyph source, null (SoftForge Stroke) or nothing (the engine default). edsnx gerber export --glyphs kicad|softforge selects 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 G02 arcs, 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.

ts
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#

ConstructHandling
%FS…%L/T zero omission, A/I absolute/incremental, any digit counts; decimal points
%MO…%, G70/G71mm / 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, G54Dnnmodal D-codes (coordinates without a D-code reuse the last one, with a warning)
G01/G02/G03, G74/G75multi-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/G37several 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/LNdeprecated: read, warned, ignored (IPNEG marks the image Negative)
RS-274Dno % 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:

  1. copper (top to bottom)
  2. masks (45 % opacity)
  3. legends
  4. pastes (50 % opacity)
  5. profile
  6. drawings
  7. 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 pad primitives.
  • Round-aperture draws become line and arc primitives.
  • Macros, polygons, apertures with holes, rectangle draws and regions become polygon primitives.

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#

CommandResult
edsnx gerber parse <file> --jsonone 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> --jsonthe 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.openSetVS 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.json sidecar to DocHost.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, with shapes, 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.

RuleSeverityWhat it measures
gbr_min_featureerrorcopper narrower than the limit (morphological opening; residues under 0.01 mm ignored)
gbr_min_clearanceerrorgap between copper islands of different nets (X2 .N; any two islands without a netlist)
gbr_min_annularerrorcopper ring around plated holes on the outer layers
gbr_sliverwarningsolder mask webs narrower than the limit (closing of the openings)
gbr_mask_openingwarningpads (pad aperture functions) without a mask opening
gbr_paste_openingwarningpaste openings more than 5 % off copper
gbr_outline_openerrorProfile segments that do not close into loops
gbr_drill_to_coppererrorholes closer than the limit to copper of another net
gbr_missing_layererrora job file entry or a usual layer (copper, mask, outline, drill) without a file
gbr_attribute_mismatchwarningfiles 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.ts buildReview):

    • 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 gerber step takes engine: 'native' (runs gerber.export) or 'kicad-cli'; the gerberCheck step (off by default in the fab job) runs gerber.check on the step’s folder with failOn: 'error' and writes a JSON report.

Commands#

CommandResult
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/ --jsonboard vs set, or set vs set (--tolerance, --shapes, --png diff.png)
edsnx gerber check fab.zip --profile jlcpcb-4 --jsonGerber 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/ --jsonthe 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.

Documentation for Electronics Designer 2.3.60.