pcb-rnd knowledge pool
Debugging an 1 pixel error in export_png
bug_pngpad by Tibor 'Igor2' Palinkas on 2018-03-04
Tags: dev, export_png, png, rounding, error, pixel, smd, pad, square
Abstract: Describe how to check the exprot_png 1 pixel rounding error on square smd pads.
After the data model rewrite, first runs on RTT turned out tons of differences, as expected: the order and even type of objects changed, e.g. square line based pads got converted into polygons. No surprise there, it will take a lot of effort to re-evaluate the new output, comparing to the old to see if anything important changed.
What was also not surprising is how the png export did not change much: bitmap renders are immune to order of objects or object types; as long as the old object looks like the new object, it's fine. But there were indeed a few differences: square pads of smd pads and pins started to have one pixel more on the bottom and the right.
A classic 1 pixel rounding error: all internal coords are in nanometer and we need to scale them down to something in the range of micrometer, depending on the DPI, when rendering into pixels of png. Most often the resulting pixel coord is not integer (i.e. falls in between two actual pixels of the png). We don't have anti-aliasing, so in this case we just need to decide which pixel to use from the two we are in between. This is normally done by the usual mathematical rounding rules of rounding to the nearest integer . But enforcing this is not automatic, and the code is often buggy, using the wrong rounding (e.g. simply truncating, or in other words rounding towards zero, which makes 4.9 not 5 but 4).
My first impression was that the data model switchover introduced a new rounding error, so I went on to debug the compat helper lib, that converts old smd pads into padstacks. I hand crafted an old .pcb file with simple, round integer mm values for geometry to ease debugging. Turned out all values were perfectly fine after the conversion, round to the nanometer, ending in all zeros.
Then I started to suspect it was really the old code that was broken, and the original png output had the pad 1 pixel smaller than it should have. Looking at the png export code of pcb-rnd 1.2.6, it's hard to say for sure: the case is handled by setting up a square bitmap as brush with the thickness of the line and asking libgd to draw a line using that brush. This also means a lot of calculations are done by libgd, not by pcb-rnd. I decided not to debug libgd.
Instead I've figured that polygons are always drawn as polygons, we have no extra thickness at the edges, so most calculations are drawn by pcb-rnd and we need to depend on libgd only for pixelating the edges of the polygon. Even more, whichever way the line drawing pixelation may do the rounding in libgd, we should trust that the final corner pixel is drawn, and that corner is specified by pcb-rnd. So I decided to add large, sharp arrows, polygon triangles, having the corner lined up with where the rendered edge of the pad should be. Then rendered the png with 1.2.6 and svn HEAD (soon to be 1.2.8) at 18000 DPI to make sure there are enough pixels to look at:
And there it is: looking at X coords, the pad perfectly lines up at 1290000nm and at 1790000nm with svn HEAD. With 1.2.6, it's just one pixel short on the right while the polygon is still rendered correctly. Same happens on Y on the bottom.
notes and tricks
- 1290000nm is calculated as: element_x + pad_x - pad_width/2: 2540000nm - 1000000nm - 500000nm/2
- Similar formula can be applied to the other corners.
- We are after pixel rounding errors, so the example is crafted in a way that it results in integer nanometer coords without rounding errors.
- the .pcb file is saved in internal units (nm), to make sure io_pcb doesn't contribute with rounding errors
- the refdes differs because of a new feature about empty "refdes text" - this difference is unrelated and should be ignored