Skip to content

[PyGerber]: Modal D03 (flash) continuation is misinterpreted as D01 (draw), producing spurious lines (reproduced on 2.4.3 and 3.0.0a4) #399

Description

PyGerber Bug Report

Note on how this report was produced

This issue was investigated and written up with the help of Claude
(Anthropic's AI assistant), while trying to debug rendering glitches in a
real-world Gerber file. The root cause was narrowed down interactively
(checking D-code counts, isolating a minimal reproduction, then checking
whether the bug was specific to the legacy Parser2 or also present in the
new Parser on the 3.0 pre-release line, etc.), and the reproduction script
/ minimal Gerber file below were also written with Claude's assistance and
verified by actually running them. I'm flagging this up front in case it's
useful context for how to weigh/triage the report; happy to answer
follow-up questions or run further tests myself.

Mandatory checks

  • I have reviewed the README for guidelines and haven't found a solution there.
  • I have reviewed the PyGerber documentation and haven't found a solution there.
  • I have reviewed the existing open issues and verified that this is not a
    duplicate.
  • I have reviewed the existing closed issues and verified that this was already
    resolved or marked as won't fix.
  • I have reviewed the existing pull requests and verified that this is not a already
    known issue.
  • I have reviewed the existing discussions and verified that this is not a already
    known issue.

I found one issue with a superficially similar symptom: #67 ("Drawing with 0
surface apertures may still create lines"), which describes unexpected
lines appearing between parts of a board and mentions "incorrect default
values for D02 and D03 coordinates" as a possible cause. It was closed as
fixed by the 2.1.0 tokenizer refactor. I believe this is a different bug
(details in Additional context below), but linking it here per the
guidelines above in case it turns out to be related after all.

To Reproduce

  1. Save the following as modal_d03_repro.gbr:

    %FSLAX33Y33*%
    %MOMM*%
    %ADD20C,2.00000*%
    G54D20*
    X010000Y010000D03*
    X050000Y050000*
    X090000Y010000D03*
    M02*
    

    This requests three flashes of a 2mm circular aperture at (10, 10),
    (50, 50) and (90, 10) mm. The second coordinate block
    (X050000Y050000*) omits the D-code, so per the Gerber Layer Format
    Specification (section 4.5, "Modal parameters") it should continue in
    D03 (flash) mode and produce a flash at (50, 50).

  2. Render it with PyGerber, e.g. with the 2.x API:

    from pygerber.gerberx3.api.v2 import GerberFile, OnParserErrorEnum
    
    parsed = GerberFile.from_file("modal_d03_repro.gbr").parse(
        on_parser_error=OnParserErrorEnum.Raise,
    )
    parsed.render_raster("modal_d03_repro_output.png", dpmm=20)

    or with the new 3.x API:

    from pygerber.gerber.api import GerberFile, FileTypeEnum
    
    gf = GerberFile.from_file("modal_d03_repro.gbr", file_type=FileTypeEnum.INFER)
    img = gf.render_with_pillow(dpmm=20)
    img.save_png("modal_d03_repro_v3_output.png")
  3. Open the resulting PNG.

  4. See a diagonal line connecting the three points, instead of three
    independent circular pads (see attached
    modal_d03_before_after.png, left side).

A self-contained script that also verifies this programmatically (by
scanning rows of the output image that should be empty if only three
separate pads exist) is attached as reproduce_modal_d03_bug.py. Running
it prints, for rows far from both pad y-coordinates, a run of ~55 drawn
pixels whose x-position shifts smoothly with y — this is the spurious
diagonal line, not part of either pad:

Row scan (should be mostly empty if only 3 separate pads exist):
  y=  84: 55 drawn px, x range [728, 782]  <-- unexpected if not at a pad
  y= 252: 55 drawn px, x range [560, 614]  <-- unexpected if not at a pad
  y= 420: 55 drawn px, x range [392, 446]  <-- unexpected if not at a pad
  y= 588: 55 drawn px, x range [224, 278]  <-- unexpected if not at a pad
  y= 756: 55 drawn px, x range [56, 110]   <-- unexpected if not at a pad
Image

modal_d03_repro.gbr.txt

reproduce_modal_d03_bug.py

Expected behavior

Three separate circular pads, with no connecting lines between them, since
the coordinate-only line should continue the previously active D03 (flash)
mode rather than being reinterpreted as D01 (draw).

I did not check this specific file against the Reference Gerber
Viewer
, but the expected behavior
follows directly from the Gerber Layer Format Specification's definition
of modal parameters (an operation code omitted in a coordinate data block
reuses whichever operation code was last set), and is also consistent with
how PyGerber itself already handles modal continuation for D01 and D02
(both work correctly; only D03 is affected — see below).

Additional context

Confirmed on two PyGerber versions

  • 2.4.3 (pip install pygerber), using
    pygerber.gerberx3.api.v2.GerberFile.render_raster.

  • 3.0.0a4 (pip install pygerber==3.0.0a4 --pre), using the new
    pygerber.gerber.api.GerberFile.render_with_pillow. I understand
    Parser2 (used by the 2.x line) has been fully replaced by a new
    Parser on the road to 3.0, so I wanted to check whether this was
    specific to the old parser before reporting — it isn't. The same
    spurious diagonal line appears with the new parser/renderer as well, and
    the same workaround (re-inserting D03, see below) resolves it there
    too:

    Row scan on 3.0.0a4 output (bug present):
      y=  84: 59 drawn px, x range [726, 784]
      y= 252: 59 drawn px, x range [558, 616]
      y= 420: 59 drawn px, x range [390, 448]
      y= 588: 59 drawn px, x range [222, 280]
      y= 756: 59 drawn px, x range [54, 112]
    drawn ratio: 0.0362
    
    Row scan on 3.0.0a4 output (after re-inserting D03):
      y=  84: empty (OK, no spurious line)
      y= 252: empty (OK, no spurious line)
      y= 420: empty (OK, no spurious line)
      y= 588: empty (OK, no spurious line)
      y= 756: empty (OK, no spurious line)
    drawn ratio: 0.0029
    

    (Pixel counts/positions differ slightly from the 2.4.3 numbers above,
    likely due to anti-aliasing / rendering differences between the two
    backends, but the pattern — a diagonal run of drawn pixels whose
    position shifts smoothly with y — is identical.)

Since the bug reproduces on both the legacy Parser2 and the new Parser,
it doesn't seem to be an artifact of one particular parser implementation,
but rather something present in the underlying logic/model for how modal
D-code state is tracked and carried across both parser generations. It
might be worth checking whether D03 is handled by a separate code path
from D01/D02 when a command's D-code is implicit / inherited from a
previous modal state.

Attached files

  • modal_d03_repro.gbr — minimal reproduction (8 lines).
  • reproduce_modal_d03_bug.py — script that renders it and programmatically
    detects the spurious line.
  • modal_d03_before_after.png — side-by-side comparison:
    • Left (current behavior): a visible diagonal line connects the
      three intended flash points instead of three independent pads.
    • Right (after applying the workaround described below): only the
      three separate circular pads are drawn, with no connecting line.

Workaround

The "after" image above was produced by taking the exact same
modal_d03_repro.gbr file and simply re-inserting an explicit D03 on the
coordinate-only line, i.e. changing X050000Y050000* to
X050000Y050000D03*:

%FSLAX33Y33*%
%MOMM*%
%ADD20C,2.00000*%
G54D20*
X010000Y010000D03*
X050000Y050000D03*
X090000Y010000D03*
M02*

Re-running the same row-scan on this corrected file confirms all five
sampled rows are empty (no spurious line), and the drawn pixel ratio drops
from ~0.034 to ~0.003 (consistent with only the three small pads
remaining). This strongly suggests the fix is simply to make the parser's
modal D-code tracking apply to D03 the same way it already does for
D01/D02.

More generally, as a workaround in our own code, we pre-process the Gerber
source before passing it to PyGerber: we track the currently active D-code
while splitting the file on *, and whenever a coordinate-only token
follows a D03 context, we explicitly re-insert D03 on that token. Modal
continuation for D01/D02 is left untouched, since it already works
correctly.

Real-world impact

This was originally discovered while rendering a much larger (~3 MB)
real-world Gerber file that makes heavy use of D03 flashes followed by
D-code-omitted coordinate blocks (a common pattern in files that keep the
D-code fully modal for long stretches of the file). In that file, this bug
produced a large number of spurious long diagonal lines crossing the
entire board image, since many unrelated flash points ended up being
connected by draw commands. Applying the pre-processing workaround
described above dropped the drawn pixel ratio in the rendered output from
~63% to ~35%, consistent with a large number of spurious lines being
removed. The source file itself is confidential (real-world PCB design),
so I'm not attaching it, but the minimal reproduction above shows the same
underlying issue.

Relationship to issue #67

While searching for existing reports, I found #67 ("Drawing with 0 surface
apertures may still create lines"), which describes a superficially
similar symptom and mentions "incorrect default values for D02 and D03
coordinates" as a possible cause. It was closed as fixed by the 2.1.0
tokenizer refactor. I believe this is a different underlying bug:

  • Drawing with 0 surface apertures may still create lines #67's working theory was about zero-width apertures still rendering a
    1-2px line (a Pillow/rounding artifact tied to D02 aperture
    repositioning), not about D-code mode being misinterpreted.
  • This report reproduces on 2.4.3 and 3.0.0a4, both well after the 2.1.0
    fix mentioned in Drawing with 0 surface apertures may still create lines #67, so whatever was fixed there does not seem to cover
    this case.
  • In my testing, modal continuation of D01 and D02 both work correctly;
    only D03 modal continuation is affected. If this were the same
    zero-width-aperture issue, I'd expect D02 repositioning itself to be the
    problem, not D03 specifically.

Happy to be told this is in fact the same root cause if it looks that way
from the implementation side.

Environment:

Please complete the following information:

  • Operating system: Ubuntu 24.04, x86_64
  • Python version: 3.12.3
  • PyGerber version: 2.4.3 (also reproduced on 3.0.0a4, see Additional context above)

Optional checks

  • I want to contribute example source files attached to this issue in the test suite
    of PyGerber for regression testing purposes.
  • I want to include separate LICENSE file for resource files attached as a result of
    an agreement described in first checkbox in this section.
  • I want to include separate README file for resource files attached as a result of
    an agreement described in first checkbox in this section.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingwaiting-for-checkboxesIssue is waiting for reporter to check all relevant checkboxes in issue template

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions